diff --git a/DjangoAdmin_Tornado_EleVue/.gitignore b/DjangoAdmin_Tornado_EleVue/.gitignore new file mode 100644 index 0000000..98a3566 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/.gitignore @@ -0,0 +1,5 @@ +.vitepress/cache +.vscode/ +node_modules +.svn +.git \ No newline at end of file diff --git a/DjangoAdmin_Tornado_EleVue/.vitepress/config/en.ts b/DjangoAdmin_Tornado_EleVue/.vitepress/config/en.ts new file mode 100644 index 0000000..a1a96ef --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/.vitepress/config/en.ts @@ -0,0 +1,51 @@ +// import type { DefaultTheme, LocaleSpecificConfig } from "vitepress"; + +// export const META_URL = "https://www.djangoadmin.cn"; +// export const META_TITLE = "DjangoAdmin"; +// export const META_DESCRIPTION = "Enterprise Admin Framework"; + +// export const enConfig: LocaleSpecificConfig = { +// 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/djangoadmin/DjangoAdmin_Fastapi_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" }, +// ], +// }, +// ], +// }, +// }, +// }; diff --git a/DjangoAdmin_Tornado_EleVue/.vitepress/config/index.ts b/DjangoAdmin_Tornado_EleVue/.vitepress/config/index.ts new file mode 100644 index 0000000..2294856 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/.vitepress/config/index.ts @@ -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", + // }, + }, +}); diff --git a/DjangoAdmin_Tornado_EleVue/.vitepress/config/shared.ts b/DjangoAdmin_Tornado_EleVue/.vitepress/config/shared.ts new file mode 100644 index 0000000..bbe394e --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/.vitepress/config/shared.ts @@ -0,0 +1,109 @@ +import { defineConfig, HeadConfig } from "vitepress"; + +export const META_IMAGE = "/social.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: "小蚂蚁云", + 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, + }, + ], + + // Top banner + [ + "script", + { + src: "/banner.js", + // @ts-expect-error: vitepress bug + async: true, + type: "text/javascript", + }, + ], + ], + + themeConfig: { + logo: "/logo.svg", + outline: [2, 3], + + socialLinks: [ + { + icon: "github", + link: "https://gitee.com/djangoadmin/DjangoAdmin_Tornado_EleVue", + }, + ], + + footer: { + copyright: + "Copyright © 2021~2026 www.djangoadmin.cn. All rights reserved.", + message: "小蚂蚁云团队 · 提供技术支持", + }, + + editLink: { + pattern: "https://gitee.com/djangoadmin/DjangoAdmin_Tornado_EleVue", + text: "对本页提出修改建议", + }, + + search: { + provider: "local", + }, + }, +}); diff --git a/DjangoAdmin_Tornado_EleVue/.vitepress/config/zh.ts b/DjangoAdmin_Tornado_EleVue/.vitepress/config/zh.ts new file mode 100644 index 0000000..84ab0e1 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/.vitepress/config/zh.ts @@ -0,0 +1,866 @@ +import type { DefaultTheme, LocaleSpecificConfig } from "vitepress"; + +export const META_URL = "https://www.djangoadmin.cn"; +export const META_TITLE = "企业级中台解决方案"; +export const META_DESCRIPTION = "企业级中台解决方案"; + +export const zhConfig: LocaleSpecificConfig = { + 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?4560d3a77ee7a45ae4e5dc4ad6cfd441"; + var s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(hm, s); + })(); + `, + ], + ], + + themeConfig: { + editLink: { + pattern: "https://gitee.com/djangoadmin/DjangoAdmin_Tornado_EleVue", + text: "对本页提出修改建议", + }, + + outline: { + label: "本页内容", + }, + + docFooter: { + prev: "上一页", + next: "下一页", + }, + + nav: [ + { text: "官网", link: "https://www.djangoadmin.cn" }, + { text: "授权", link: "https://www.djangoadmin.cn/goods/detail/2" }, + { + text: "部署文档", + items: [ + { + text: "DjangoAdmin混编版Django+Layui版本", + link: "http://docs.django.layui.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离Django+ElementPlus版本", + link: "http://docs.django.elevue.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离Django+AntDesign版本", + link: "http://docs.django.antdvue.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离Django+NaiveUI版本", + link: "http://docs.django.naivevue.djangoadmin.cn", + }, + { + text: "DjangoAdmin混编版Flask+Layui版本", + link: "http://docs.flask.layui.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离Flask+ElementPlus版本", + link: "http://docs.flask.elevue.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离Flask+AntDesign版本", + link: "http://docs.flask.antdvue.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离Flask+NaiveUI版本", + link: "http://docs.flask.naivevue.djangoadmin.cn", + }, + { + text: "DjangoAdmin混编版FastAPI+Layui版本", + link: "http://docs.fastapi.layui.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离FastAPI+ElementPlus版本", + link: "http://docs.fastapi.elevue.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离FastAPI+AntDesign版本", + link: "http://docs.fastapi.antdvue.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离FastAPI+NaiveUI版本", + link: "http://docs.fastapi.naivevue.djangoadmin.cn", + }, + { + text: "DjangoAdmin混编版Tornado+Layui版本", + link: "http://docs.tornado.layui.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离Tornado+ElementPlus版本", + link: "http://docs.tornado.elevue.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离Tornado+AntDesign版本", + link: "http://docs.tornado.antdvue.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离Tornado+NaiveUI版本", + link: "http://docs.tornado.naivevue.djangoadmin.cn", + }, + ], + }, + { + text: "在线演示", + items: [ + { + text: "DjangoAdmin混编版Django+Layui版本", + link: "http://manage.django.layui.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离Django+ElementPlus版本", + link: "http://manage.django.elevue.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离Django+AntDesign版本", + link: "http://manage.django.antdvue.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离Django+NaiveUI版本", + link: "http://manage.django.naivevue.djangoadmin.cn", + }, + { + text: "DjangoAdmin混编版Flask+Layui版本", + link: "http://manage.flask.layui.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离Flask+ElementPlus版本", + link: "http://manage.flask.elevue.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离Flask+AntDesign版本", + link: "http://manage.flask.antdvue.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离Flask+NaiveUI版本", + link: "http://manage.flask.naivevue.djangoadmin.cn", + }, + { + text: "DjangoAdmin混编版FastAPI+Layui版本", + link: "http://manage.fastapi.layui.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离FastAPI+ElementPlus版本", + link: "http://manage.fastapi.elevue.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离FastAPI+AntDesign版本", + link: "http://manage.fastapi.antdvue.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离FastAPI+NaiveUI版本", + link: "http://manage.fastapi.naivevue.djangoadmin.cn", + }, + { + text: "DjangoAdmin混编版Tornado+Layui版本", + link: "http://manage.tornado.layui.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离Tornado+ElementPlus版本", + link: "http://manage.tornado.elevue.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离Tornado+AntDesign版本", + link: "http://manage.tornado.antdvue.djangoadmin.cn", + }, + { + text: "DjangoAdmin前后端分离Tornado+NaiveUI版本", + link: "http://manage.tornado.naivevue.djangoadmin.cn", + }, + ], + }, + { + text: "广告赞助", + link: "/zh/other/sponsor.html", + }, + { + text: "相关链接", + items: [ + { + text: "官方网站", + link: "https://www.djangoadmin.cn", + }, + { + text: "Gitee 仓库", + link: "https://gitee.com/djangoadmin", + }, + { + text: "Tornado 文档", + link: "https://www.tornadoweb.org/en/stable/", + }, + { + 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/struct.html" }, + { text: "1.4 新手入门指南", link: "/zh/course.html" }, + { text: "1.5 更新日志", link: "/zh/changelog.html" }, + ], + }, + + // ================================================================ + // 第 2 章:快速入门 + // ================================================================ + { + text: "🚀 2. 快速入门", + collapsed: false, + items: [ + { text: "2.1 开发环境准备", link: "/zh/volume1/environment.html" }, + { text: "2.2 后端服务启动", link: "/zh/volume1/backend.html" }, + { text: "2.3 前端服务启动", link: "/zh/volume1/frontend.html" }, + { text: "2.4 数据库初始化", link: "/zh/volume1/database.html" }, + { text: "2.5 Docker 快速部署", link: "/zh/volume1/docker.html" }, + { text: "2.6 环境变量配置参考", link: "/zh/volume1/env-vars.html" }, + { text: "2.7 第一个接口调试", link: "/zh/volume1/first-api.html" }, + { text: "2.8 常见问题 FAQ", link: "/zh/volume1/faq.html" }, + ], + }, + + // ================================================================ + // 第 3 章:模块开发实战(独立成章,新人快速上手) + // ================================================================ + { + text: "🧪 3. 模块开发实战", + collapsed: false, + items: [ + { text: "3.1 概述与目标", link: "/zh/volume2/overview.html" }, + { text: "3.2 数据模型定义", link: "/zh/volume2/model.html" }, + { + text: "3.3 参数验证(Pydantic Schema)", + link: "/zh/volume2/schema.html", + }, + { + text: "3.4 数据访问层(Repository)", + link: "/zh/volume2/repository.html", + }, + { + text: "3.5 业务逻辑层(Service)", + link: "/zh/volume2/service.html", + }, + { + text: "3.6 请求处理器(Handler)", + link: "/zh/volume2/handler.html", + }, + { text: "3.7 路由注册", link: "/zh/volume2/router.html" }, + { text: "3.8 权限节点配置", link: "/zh/volume2/permission.html" }, + { text: "3.9 前端 API 层开发", link: "/zh/volume2/fe-api.html" }, + { text: "3.10 前端页面视图开发", link: "/zh/volume2/fe-view.html" }, + { + text: "3.11 前端路由与菜单配置", + link: "/zh/volume2/fe-router.html", + }, + { text: "3.12 完整代码清单", link: "/zh/volume2/code-list.html" }, + { text: "3.13 常见问题与排错", link: "/zh/volume2/faq.html" }, + { text: "3.14 示例模块(Example)", link: "/zh/volume2/example.html" }, + ], + }, + + // ================================================================ + // 第 4 章:架构设计 + // ================================================================ + { + text: "🏛️ 4. 架构设计", + collapsed: false, + items: [ + { text: "4.1 整体架构概览", link: "/zh/volume3/overview.html" }, + { text: "4.2 技术选型与决策", link: "/zh/volume3/tech-stack.html" }, + { text: "4.3 请求处理链路", link: "/zh/volume3/request-flow.html" }, + { text: "4.4 分层架构详解", link: "/zh/volume3/layer-arch.html" }, + { text: "4.5 目录结构设计", link: "/zh/volume3/directory.html" }, + { text: "4.6 核心基类体系", link: "/zh/volume3/base-classes.html" }, + { + text: "4.7 Tornado IOLoop 与异步模型", + link: "/zh/volume3/tornado-async.html", + }, + { text: "4.8 缓存架构设计", link: "/zh/volume3/cache.html" }, + { + text: "4.9 数据库设计(多驱动)", + link: "/zh/volume3/database-design.html", + }, + { text: "4.10 安全架构设计", link: "/zh/volume3/security.html" }, + { text: "4.11 扩展性设计", link: "/zh/volume3/extensibility.html" }, + ], + }, + + // ================================================================ + // 第 5 章:开发指南 + // ================================================================ + { + text: "🛠️ 5. 开发指南", + collapsed: false, + items: [ + // ------------------------------------------------------------ + // 5.1 后端核心功能(按类别分组) + // ------------------------------------------------------------ + { + text: "5.1 后端核心功能", + collapsed: false, + items: [ + // 🔐 认证与安全 + { + text: "🔐 认证与安全", + collapsed: false, + items: [ + { + text: "5.1.1 用户认证(JWT)", + link: "/zh/volume4/core/jwt.html", + }, + { + text: "5.1.2 认证流程实战", + link: "/zh/volume4/core/auth-flow.html", + }, + { + text: "5.1.3 验证码机制", + link: "/zh/volume4/core/captcha.html", + }, + { + text: "5.1.4 权限管理(RBAC)", + link: "/zh/volume4/core/rbac.html", + }, + { + text: "5.1.5 跨域资源共享(CORS)", + link: "/zh/volume4/core/cors.html", + }, + { + text: "5.1.6 演示模式", + link: "/zh/volume4/core/demo-mode.html", + }, + ], + }, + // 📊 数据与配置 + { + text: "📊 数据与配置", + collapsed: false, + items: [ + { + text: "5.1.7 字典模块", + link: "/zh/volume4/core/dict.html", + }, + { + text: "5.1.8 字典配置与使用", + link: "/zh/volume4/core/dict-guide.html", + }, + { + text: "5.1.9 系统配置管理", + link: "/zh/volume4/core/config.html", + }, + { + text: "5.1.10 参数验证(Pydantic)", + link: "/zh/volume4/core/validator.html", + }, + ], + }, + // 📝 日志与监控 + { + text: "📝 日志与监控", + collapsed: false, + items: [ + { + text: "5.1.11 操作日志", + link: "/zh/volume4/core/operation-log.html", + }, + { + text: "5.1.12 登录日志", + link: "/zh/volume4/core/login-log.html", + }, + { + text: "5.1.13 异常与响应处理", + link: "/zh/volume4/core/exception.html", + }, + ], + }, + // 🛡️ 防护与治理 + { + text: "🛡️ 防护与治理", + collapsed: false, + items: [ + { + text: "5.1.14 登录防爆破", + link: "/zh/volume4/core/login-protect.html", + }, + { + text: "5.1.15 链路追踪(TraceID)", + link: "/zh/volume4/core/trace-id.html", + }, + { + text: "5.1.16 优雅关闭", + link: "/zh/volume4/core/graceful-shutdown.html", + }, + ], + }, + ], + }, + + // ------------------------------------------------------------ + // 5.2 后端业务模块(功能索引,不重复 CRUD 细节) + // ------------------------------------------------------------ + { + text: "5.2 后端业务模块", + collapsed: false, + items: [ + { + text: "5.2.1 用户管理", + link: "/zh/volume4/business/user.html", + }, + { + text: "5.2.2 角色管理", + link: "/zh/volume4/business/role.html", + }, + { + text: "5.2.3 菜单管理", + link: "/zh/volume4/business/menu.html", + }, + { + text: "5.2.4 部门/职级/岗位管理", + link: "/zh/volume4/business/dept.html", + }, + { + text: "5.2.5 文章管理", + link: "/zh/volume4/business/article.html", + }, + { + text: "5.2.6 公告与链接管理", + link: "/zh/volume4/business/content.html", + }, + { + text: "5.2.7 行政区划管理", + link: "/zh/volume4/business/city.html", + }, + { + text: "5.2.8 分类管理", + link: "/zh/volume4/business/category.html", + }, + { + text: "5.2.9 定时任务", + link: "/zh/volume4/business/job.html", + }, + { + text: "5.2.10 参数管理", + link: "/zh/volume4/business/param.html", + }, + { + text: "5.2.11 文件模板管理", + link: "/zh/volume4/business/file-template.html", + }, + { + text: "5.2.12 定时任务日志", + link: "/zh/volume4/business/job-log.html", + }, + ], + }, + + // ------------------------------------------------------------ + // 5.3 后端通用工具(按类别分组) + // ------------------------------------------------------------ + { + text: "5.3 后端通用工具", + collapsed: false, + items: [ + // 📁 文件处理 + { + text: "📁 文件处理", + collapsed: false, + items: [ + { + text: "5.3.1 文件上传模块", + link: "/zh/volume4/tools/upload.html", + }, + { + text: "5.3.2 上传配置与使用", + link: "/zh/volume4/tools/upload-guide.html", + }, + { + text: "5.3.3 富文本处理", + link: "/zh/volume4/tools/rich-text.html", + }, + { + text: "5.3.4 数据导入导出(Excel)", + link: "/zh/volume4/tools/excel.html", + }, + ], + }, + // 🔧 核心工具 + { + text: "🔧 核心工具", + collapsed: false, + items: [ + { + text: "5.3.5 数据库会话管理", + link: "/zh/volume4/tools/db-session.html", + }, + { + text: "5.3.6 统一响应封装(R 对象)", + link: "/zh/volume4/tools/response.html", + }, + { + text: "5.3.7 密码加密与验证(双盐 bcrypt)", + link: "/zh/volume4/tools/password.html", + }, + { + text: "5.3.8 日志系统(JSON 结构化)", + link: "/zh/volume4/tools/logger.html", + }, + { + text: "5.3.9 请求上下文(ContextVar)", + link: "/zh/volume4/tools/context.html", + }, + ], + }, + // 🧩 辅助工具 + { + text: "🧩 辅助工具", + collapsed: false, + items: [ + { + text: "5.3.10 字典缓存工具", + link: "/zh/volume4/tools/dict-util.html", + }, + { + text: "5.3.11 IP 归属地解析", + link: "/zh/volume4/tools/ip-region.html", + }, + { + text: "5.3.12 权限缓存", + link: "/zh/volume4/tools/perm-cache.html", + }, + { + text: "5.3.13 通用 Helper 函数", + link: "/zh/volume4/tools/helpers.html", + }, + ], + }, + ], + }, + + // ------------------------------------------------------------ + // 5.4 前端开发(Vue3 + Element Plus) + // ------------------------------------------------------------ + { + text: "5.4 前端开发", + collapsed: false, + items: [ + { + text: "5.4.1 环境搭建与快速上手", + link: "/zh/volume4/frontend/setup.html", + }, + { + text: "5.4.2 API 层开发", + link: "/zh/volume4/frontend/api.html", + }, + { + text: "5.4.3 页面视图开发", + link: "/zh/volume4/frontend/view.html", + }, + { + text: "5.4.4 数据表格组件", + link: "/zh/volume4/frontend/table.html", + }, + { + text: "5.4.5 表单组件", + link: "/zh/volume4/frontend/form.html", + }, + { + text: "5.4.6 弹窗组件", + link: "/zh/volume4/frontend/modal.html", + }, + { + text: "5.4.7 权限指令 v-perm", + link: "/zh/volume4/frontend/perm-directive.html", + }, + { + text: "5.4.8 路由与菜单", + link: "/zh/volume4/frontend/router.html", + }, + { + text: "5.4.9 状态管理(Pinia)", + link: "/zh/volume4/frontend/store.html", + }, + { + text: "5.4.10 公共组件库", + link: "/zh/volume4/frontend/components.html", + }, + { + text: "5.4.11 组合式函数(Hooks)", + link: "/zh/volume4/frontend/hooks.html", + }, + { + text: "5.4.12 工具函数", + link: "/zh/volume4/frontend/utils.html", + }, + { + text: "5.4.13 样式与主题", + link: "/zh/volume4/frontend/style.html", + }, + { + text: "5.4.14 前端构建", + link: "/zh/volume4/frontend/build.html", + }, + ], + }, + + // ------------------------------------------------------------ + // 5.5 前后端联调 + // ------------------------------------------------------------ + { + text: "5.5 前后端联调", + collapsed: false, + items: [ + { + text: "5.5.1 接口响应规范", + link: "/zh/volume4/integration/api-contract.html", + }, + { + text: "5.5.2 代理配置", + link: "/zh/volume4/integration/proxy.html", + }, + { + text: "5.5.3 权限节点联动", + link: "/zh/volume4/integration/perm-sync.html", + }, + { + text: "5.5.4 文件上传联调", + link: "/zh/volume4/integration/upload-sync.html", + }, + { + text: "5.5.5 字典数据联调", + link: "/zh/volume4/integration/dict-sync.html", + }, + ], + }, + + // ------------------------------------------------------------ + // 5.6 API 接口速查表 + // ------------------------------------------------------------ + { + text: "5.6 API 接口速查表", + link: "/zh/volume4/api-cheatsheet.html", + }, + ], + }, + + // ================================================================ + // 第 6 章:代码生成器(✨ 亮点独立成章) + // ================================================================ + { + text: "🔧 6. 代码生成器", + collapsed: false, + items: [ + { text: "6.1 概述与架构", link: "/zh/volume5/overview.html" }, + { text: "6.2 CLI 命令行使用", link: "/zh/volume5/cli.html" }, + { text: "6.3 Web 管理界面", link: "/zh/volume5/web-ui.html" }, + { text: "6.4 表结构解析引擎", link: "/zh/volume5/parser.html" }, + { text: "6.5 代码生成引擎", link: "/zh/volume5/code-engine.html" }, + { text: "6.6 自定义模板开发", link: "/zh/volume5/templates.html" }, + { text: "6.7 配置文件详解", link: "/zh/volume5/config.html" }, + { text: "6.8 实战案例", link: "/zh/volume5/demo.html" }, + { text: "6.9 常见问题", link: "/zh/volume5/faq.html" }, + ], + }, + + // ================================================================ + // 第 7 章:运维部署(Checklist 前置) + // ================================================================ + { + text: "⚙️ 7. 运维部署", + collapsed: false, + items: [ + { + text: "7.1 部署 Checklist", + link: "/zh/volume6/deploy-checklist.html", + }, + { text: "7.2 环境规划", link: "/zh/volume6/env-plan.html" }, + { text: "7.3 数据库迁移(Alembic)", link: "/zh/volume6/db-migrate.html" }, + { text: "7.4 跨库数据迁移", link: "/zh/volume6/db-cross-migrate.html" }, + { text: "7.5 Docker 容器化", link: "/zh/volume6/docker.html" }, + { text: "7.6 Nginx 反向代理", link: "/zh/volume6/nginx.html" }, + { + text: "7.7 Supervisor 进程管理", + link: "/zh/volume6/supervisor.html", + }, + { text: "7.8 健康检查与监控", link: "/zh/volume6/monitoring.html" }, + { + text: "7.9 性能调优与基线", + link: "/zh/volume6/performance.html", + }, + { + text: "7.10 安全加固清单", + link: "/zh/volume6/security-hardening.html", + }, + { text: "7.11 备份与恢复", link: "/zh/volume6/backup.html" }, + { text: "7.12 版本升级与回滚", link: "/zh/volume6/upgrade.html" }, + { + text: "7.13 故障排查手册", + link: "/zh/volume6/troubleshooting.html", + }, + ], + }, + + // ================================================================ + // 第 8 章:规范标准(加标签/分组) + // ================================================================ + { + text: "📋 8. 规范标准", + collapsed: false, + items: [ + // ⭐ 必须遵守 + { + text: "⭐ 必须遵守", + collapsed: false, + items: [ + { + text: "8.1 API 响应规范", + link: "/zh/volume7/api-response.html", + }, + { + text: "8.2 分页查询规范", + link: "/zh/volume7/pagination.html", + }, + { text: "8.3 Git 提交规范", link: "/zh/volume7/git.html" }, + ], + }, + // 📌 建议遵守 + { + text: "📌 建议遵守", + collapsed: false, + items: [ + { text: "8.4 数据删除规范", link: "/zh/volume7/deletion.html" }, + { + text: "8.5 Repository 层规范", + link: "/zh/volume7/repository.html", + }, + { + text: "8.6 Service 层规范", + link: "/zh/volume7/service.html", + }, + { + text: "8.7 Handler 层规范", + link: "/zh/volume7/handler.html", + }, + { + text: "8.8 Pydantic Schema 规范", + link: "/zh/volume7/pydantic.html", + }, + { + text: "8.9 前端组件规范", + link: "/zh/volume7/fe-component.html", + }, + { + text: "8.10 前端 API 模块规范", + link: "/zh/volume7/fe-api.html", + }, + ], + }, + // 📖 参考即可 + { + text: "📖 参考即可", + collapsed: false, + items: [ + { text: "8.11 测试规范", link: "/zh/volume7/testing.html" }, + { + text: "8.12 代码审查清单", + link: "/zh/volume7/code-review.html", + }, + { + text: "8.13 文档规范", + link: "/zh/volume7/doc-standard.html", + }, + ], + }, + ], + }, + // ============================================ + // 其他 + // ============================================ + { + text: "📄 9. 其他", + collapsed: false, + items: [ + { text: "9.1 常见问题汇总", link: "/zh/faq.html" }, + { text: "9.2 免责声明", link: "/zh/other/declare.html" }, + { + text: "9.3 开源协议", + link: "https://gitee.com/djangoadmin/DjangoAdmin_Tornado_EleVue/blob/master/LICENSE", + }, + { text: "9.4 广告赞助", link: "/zh/other/sponsor.html" }, + ], + }, + ], + }, + }, +}; + +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: "点击反馈", + }, + }, + }, + }, +}; diff --git a/DjangoAdmin_Tornado_EleVue/.vitepress/theme/components/AsideSponsors.vue b/DjangoAdmin_Tornado_EleVue/.vitepress/theme/components/AsideSponsors.vue new file mode 100644 index 0000000..aaa647c --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/.vitepress/theme/components/AsideSponsors.vue @@ -0,0 +1,151 @@ + + + + + diff --git a/DjangoAdmin_Tornado_EleVue/.vitepress/theme/components/HomeSponsors.vue b/DjangoAdmin_Tornado_EleVue/.vitepress/theme/components/HomeSponsors.vue new file mode 100644 index 0000000..5cd6011 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/.vitepress/theme/components/HomeSponsors.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/DjangoAdmin_Tornado_EleVue/.vitepress/theme/components/HomeSponsorsGroup.vue b/DjangoAdmin_Tornado_EleVue/.vitepress/theme/components/HomeSponsorsGroup.vue new file mode 100644 index 0000000..7a7e9b1 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/.vitepress/theme/components/HomeSponsorsGroup.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/DjangoAdmin_Tornado_EleVue/.vitepress/theme/components/VideoModuleLink.vue b/DjangoAdmin_Tornado_EleVue/.vitepress/theme/components/VideoModuleLink.vue new file mode 100644 index 0000000..864ff86 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/.vitepress/theme/components/VideoModuleLink.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/DjangoAdmin_Tornado_EleVue/.vitepress/theme/components/sponsors.json b/DjangoAdmin_Tornado_EleVue/.vitepress/theme/components/sponsors.json new file mode 100644 index 0000000..e2bc24c --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/.vitepress/theme/components/sponsors.json @@ -0,0 +1,38 @@ +{ + "platinum": [], + "gold": [], + "silver": [ + { + "alt": "企业级中台解决方案", + "href": "https://www.djangoadmin.cn", + "imgSrcDark": "/sponsor/djangoadmin.png", + "imgSrcLight": "/sponsor/djangoadmin.png" + }, + { + "alt": "企业级中台解决方案", + "href": "https://www.djangoadmin.cn", + "imgSrcDark": "/sponsor/djangoadmin.png", + "imgSrcLight": "/sponsor/djangoadmin.png" + } + ], + "bronze": [ + { + "alt": "企业级中台解决方案", + "href": "https://www.djangoadmin.cn", + "imgSrcDark": "/sponsor/djangoadmin.png", + "imgSrcLight": "/sponsor/djangoadmin.png" + }, + { + "alt": "企业级中台解决方案", + "href": "https://www.djangoadmin.cn", + "imgSrcDark": "/sponsor/djangoadmin.png", + "imgSrcLight": "/sponsor/djangoadmin.png" + }, + { + "alt": "企业级中台解决方案", + "href": "https://www.djangoadmin.cn", + "imgSrcDark": "/sponsor/djangoadmin.png", + "imgSrcLight": "/sponsor/djangoadmin.png" + } + ] +} diff --git a/DjangoAdmin_Tornado_EleVue/.vitepress/theme/index.ts b/DjangoAdmin_Tornado_EleVue/.vitepress/theme/index.ts new file mode 100644 index 0000000..efc6f00 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/.vitepress/theme/index.ts @@ -0,0 +1,36 @@ +import { h } from "vue"; +import { type Theme, useRoute } from "vitepress"; +import DefaultTheme from "vitepress/theme"; +import AsideSponsors from "./components/AsideSponsors.vue"; +import VideoModuleLink from "./components/VideoModuleLink.vue"; +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: "小蚂蚁云", + }), + ), + "aside-ads-before": () => h(AsideSponsors), + }); + }, + setup() { + const route = useRoute(); + imageViewer(route); + }, + enhanceApp({ app }) { + app.component("VideoModuleLink", VideoModuleLink); + app.component("vImageViewer", vImageViewer); + }, +}; + +export default theme; diff --git a/DjangoAdmin_Tornado_EleVue/.vitepress/theme/styles/home-links.css b/DjangoAdmin_Tornado_EleVue/.vitepress/theme/styles/home-links.css new file mode 100644 index 0000000..01e3777 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/.vitepress/theme/styles/home-links.css @@ -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); +} diff --git a/DjangoAdmin_Tornado_EleVue/.vitepress/theme/styles/vars.css b/DjangoAdmin_Tornado_EleVue/.vitepress/theme/styles/vars.css new file mode 100644 index 0000000..8f1833a --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/.vitepress/theme/styles/vars.css @@ -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; +} diff --git a/DjangoAdmin_Tornado_EleVue/.vitepress/translation-status.json b/DjangoAdmin_Tornado_EleVue/.vitepress/translation-status.json new file mode 100644 index 0000000..e15a3c7 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/.vitepress/translation-status.json @@ -0,0 +1,6 @@ +{ + "zh": { + "hash": "3.0.0", + "date": "2026-09-07" + } +} diff --git a/DjangoAdmin_Tornado_EleVue/README.md b/DjangoAdmin_Tornado_EleVue/README.md new file mode 100644 index 0000000..c55ab63 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/README.md @@ -0,0 +1,34 @@ +# DjangoAdmin 文档 + +基于 VitePress 构建的项目开发者文档站点。 + +## 本地开发 + +```bash +# 安装依赖 +pnpm install + +# 启动开发服务器 +pnpm docs + +# 构建静态站点 +pnpm docs:build + +# 预览构建结果 +pnpm docs:preview +``` + +## 文档结构 + +``` +zh/ +├── introduction.md # 项目介绍 +├── volume1/ # 快速入门 +├── volume2/ # 模块开发实战 +├── volume3/ # 架构设计 +├── volume4/ # 开发指南 +├── volume5/ # 代码生成器 +├── volume6/ # 运维部署 +├── volume7/ # 规范标准 +└── other/ # 其他 +``` diff --git a/DjangoAdmin_Tornado_EleVue/index.md b/DjangoAdmin_Tornado_EleVue/index.md new file mode 100644 index 0000000..94893c0 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/index.md @@ -0,0 +1,95 @@ +--- +layout: home + +title: DjangoAdmin文档 +titleTemplate: 前后端分离Tornado+ElementPlus版 + +hero: + name: DjangoAdmin + text: "企业级中台解决方案Tornado+ElementPlus版" + tagline: "赋能开发者,助力企业发展。\n企业级数据平台底座 · 让数字化触手可及。" + image: + src: /logo.svg + alt: DjangoAdmin + actions: + - theme: brand + text: 开始使用 + link: /zh/introduction + - theme: alt + text: 在线演示 + link: http://manage.tornado.elevue.djangoadmin.cn + - theme: alt + text: 官方授权 + link: https://www.djangoadmin.cn/goods/detail/2 + - theme: alt + text: ❤️ 成为赞助商 + link: /zh/other/sponsor.html + +features: + - title: 🎨 开箱即用的主题配色 + details: 支持开箱即用的多种主题配色,并提供灵活的配置选项,轻松切换主题颜色方案,满足品牌或个人偏好的视觉需求。 + link: /zh/volume4/frontend/style + linkText: 查看更多 + rel: noreferrer + - title: 🗄️ 支持多种数据库 + details: 提供强大的多数据库支持,可轻松集成 MySQL、PostgreSQL、SQL Server、SQLite、Oracle 等多种数据库,灵活切换数据源,满足复杂业务需求。 + link: /zh/volume3/extensibility + linkText: 查看更多 + rel: noreferrer + - title: 🚀 全新技术栈 + details: Tornado 6.x、SQLAlchemy 2.0、Pydantic v2、Vue3、Element Plus 等技术栈,采用主流前后端分离架构实现。 + link: /zh/volume3/tech-stack + linkText: 查看更多 + rel: noreferrer + - title: 📚 丰富的模块 + details: 集成丰富的基础功能,如权限架构体系、日志、字典、配置、参数、通知公告、定时任务等功能。 + link: /zh/introduction + linkText: 查看更多 + rel: noreferrer + - title: 🔧 开发工具支持 + details: 内置代码生成器,支持自定义模板,一键生成前后端业务模块代码,大幅提升开发效率,实现敏捷迭代。 + link: /zh/volume5/overview + linkText: 查看更多 + rel: noreferrer + - title: 🧩 高可扩展性 + details: 预留丰富的扩展接口,核心模块与配置文件支持按需加载,便于二次开发,轻松集成第三方服务或自定义组件。 + link: /zh/volume3/extensibility + linkText: 查看更多 + rel: noreferrer + - title: 🏗️ 前端工程化实践 + details: 基于 Vite 构建,集成 ESLint + Prettier 代码规范,配合 TypeScript 类型安全,保障代码质量与团队协作效率。 + link: /zh/volume4/frontend/build + linkText: 查看更多 + rel: noreferrer + - title: 📋 操作日志与审计追踪 + details: 自动记录用户操作行为与系统运行日志,支持全链路追踪与异常告警,方便问题排查与安全审计,满足合规性要求。 + link: /zh/volume4/core/operation-log + linkText: 查看更多 + rel: noreferrer + - title: ⏰ 定时任务调度 + details: 内置轻量级任务调度器,支持 Cron 表达式配置定时任务,可灵活编排后端脚本与数据同步流程,实现自动化运维。 + link: /zh/volume4/business/job + linkText: 查看更多 + rel: noreferrer + - title: 📊 数据导出与报表 + details: 支持 Excel 等格式的数据导入导出,可定制报表模板,满足运营统计与数据归档需求。 + link: /zh/volume4/tools/excel + linkText: 查看更多 + rel: noreferrer + - title: 🐳 容器化部署支持 + details: 提供 Docker 镜像部署脚本,支持一键容器化部署与弹性伸缩,简化运维流程,降低部署门槛。 + link: /zh/volume6/docker + linkText: 查看更多 + rel: noreferrer + - title: 🛡️ 数据校验与异常处理 + details: 集成 Pydantic 数据校验与全局异常拦截,统一接口返回格式,保障数据安全,提升接口健壮性。 + link: /zh/volume4/core/exception + linkText: 查看更多 + rel: noreferrer +--- + + + + diff --git a/DjangoAdmin_Tornado_EleVue/package.json b/DjangoAdmin_Tornado_EleVue/package.json new file mode 100644 index 0000000..77bd941 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/package.json @@ -0,0 +1,24 @@ +{ + "name": "@djangoadmin/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" + } +} diff --git a/DjangoAdmin_Tornado_EleVue/public/banner.js b/DjangoAdmin_Tornado_EleVue/public/banner.js new file mode 100644 index 0000000..ec3ef5b --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/public/banner.js @@ -0,0 +1,257 @@ +((window.BitterBrainsBanner = window.BitterBrainsBanner || { + host: "", + affiliateKey: "vuerouter", + emulateDate: void 0, + emulateHour: void 0, + date: "03/09/2025, 01:30 PM Central European Standard Time", + activePhase: { + id: "VS-FREEWEEKEND25_AFFILIATE_SUPERSALE_EXT8", + countdownEnds: "2025-03-09T22:59:59.000000Z", + cta: "获取官方授权", + ends: "2025-03-09T22:59:59.000000Z", + title: "新品首发,限时特惠,即刻抢购! ", + link: "https://www.djangoadmin.cn", + assets: null, + static: null, + classes: ["bb-campaign-vs-affiliates-freeweekend-urgent"], + prelude: "小蚂蚁云 · 企业级旗舰产品", + // addendum: "Extended for", + subtitle: "赋能开发者,驱动企业增长,提供一站式数据中台解决方案。", + couponCode: null, + isExtended: !1, + hideBanners: !1, + assetsFormat: "png", + showCountdown: 0, + countdownFooter: null, + showDaysAsHours: !0, + affiliates: [ + "vuerouter", + "pinia", + "vueuse", + "vuejsforge", + "nuxtnation", + "vuejsdevelopers", + "VUEDEMY", + "vs_masteringpinia", + "vuejsnation", + "SENODEV", + "NSCRIPT", + ], + interlude: null, + starts: "2025-03-08T22:59:59.000Z", + isRunning: !0, + remaining: 37757670, + closedKey: "VS-FREEWEEKEND25_AFFILIATE_SUPERSALE_EXT8_CLOSED", + countdownRemaining: 37757670, + overrides: { vuejs: { hideSeconds: !0 } }, + logoMobile: "/banners/mark.svg", + logoTablet: "/banners/mark.svg", + logoDesktop: "/banners/logo.svg", + }, + fromCache: !1, + cacheTtl: 0, + type: "top", + closeable: void 0, + isOpen: !1, + countdownInterval: null, + isInitiated: !1, + style: null, + allowList: { + masteringnuxt: (n) => "/" === n || "/nuxt2" === n || "/nuxt3" === n, + vueschool: (n) => + "/" === n || + n.startsWith("/courses") || + n.startsWith("/articles") || + n.startsWith("/lessons") || + n.startsWith("/vueschool-demo"), + vueschool_free: (n) => + "/" === n || + n.startsWith("/courses") || + n.startsWith("/articles") || + n.startsWith("/lessons"), + vueschool_premium: (n) => + "/" === n || + n.startsWith("/courses") || + n.startsWith("/articles") || + n.startsWith("/lessons"), + vs_frontendnation: (n) => "/" === n, + }, + render() { + const { + countdownFooter: n, + prelude: e, + interlude: t, + addendum: b, + title: a, + subtitle: i, + cta: o, + link: r, + classes: s, + showCountdown: l, + } = this.activePhase, + d = n ? `` : "", + c = `\n
\n \n
\n
\n
\n ${ + e ? `
${e}
` : "" + }\n
\n
${a}
\n ${ + i ? `
${i}
` : "" + }\n
\n ${ + t ? `
${t}
` : "" + }\n
\n ${ + o + ? `
${o}
` + : "" + }\n
\n ${ + b ? `
${b}
` : "" + }\n ${ + l + ? `
${this.renderCountdown()}
${d}
` + : "" + }\n
\n
\n
\n \n \n \n
\n `, + p = document.createElement("a"); + p.setAttribute("id", "bb-banner"); + const u = { + utm_source: this.affiliateKey, + utm_medium: "website", + utm_campaign: "affiliate", + utm_content: `${this.type}_banner`, + banner_type: this.type, + }, + g = "internal" in this.activePhase && !!this.activePhase?.internal; + (!g && p.setAttribute("target", "_blank"), + "vueschool" !== this.affiliateKey && + (u.friend = this.affiliateKey?.toUpperCase())); + const h = new URLSearchParams(g ? {} : u).toString(); + (p.setAttribute("href", `${r}${g ? "" : "?" + h}`), + p.classList.add(...s), + (p.innerHTML = c), + this.addBodyClasses(), + (function () { + let n = document.querySelector("#bb-banner-video"); + (n || + (n = document.querySelector("#bb-banner-container") || document.body), + n.append(p)); + })(), + addEventListener("popstate", (n) => { + this.handleNavigation(); + }), + (window.history.pushState = new Proxy(window.history.pushState, { + apply: (n, e, t) => { + const b = n.apply(e, t); + return (this.handleNavigation(), b); + }, + }))); + }, + handleNavigation() { + this.addBodyClasses(); + }, + addBodyClasses() { + if (!this.isOpen) return; + const n = document.querySelectorAll("html")[0]; + (n.classList.add("has-bb-banner"), + n.classList.add(`bb-type-${this.type}`), + this.affiliateKey && n.classList.add(`bb-${this.affiliateKey}`)); + const e = window.location.pathname || "/", + t = !!window.BitterBrainsBanner.emulateDate; + !this.allowList[this.affiliateKey] || + this.allowList[this.affiliateKey](e) || + t + ? n.classList.remove("bb-banner-hidden") + : n.classList.add("bb-banner-hidden"); + }, + renderCountdown() { + const { showDaysAsHours: n, hideSeconds: e } = this.activePhase, + t = ["hours", "minutes"]; + return ( + n || t.unshift("days"), + e || t.push("seconds"), + t.map((n) => this.renderCountdownPart(n)).join("") + ); + }, + renderCountdownPart: (n) => + `\n
\n
\n
${ + n[0] + }${n.slice( + 1, + )}
\n
\n
:
\n
\n `, + getType() { + if (document.querySelector("#bb-banner-video")) return "video"; + return document.querySelector("#bb-banner-container") + ? "inline" + : this.type && ["top", "bottom"].includes(this.type) + ? this.type + : "top"; + }, + insertStyleTag() { + const n = document.createElement("style"); + ((n.textContent = window.BitterBrainsBanner.style), + document.head.append(n)); + }, + close() { + document.querySelector("#bb-banner").remove(); + const n = document.querySelectorAll("html")[0]; + (n.classList.remove("has-bb-banner"), + n.classList.remove("bb-banner-container"), + clearInterval(this.countdownInterval), + (this.isOpen = !1)); + }, + bindCloseButton() { + document.querySelector("#bb-close").addEventListener("click", (n) => { + (n.preventDefault(), + n.stopPropagation(), + this.close(), + localStorage.setItem(this.activePhase.closedKey, 1), + localStorage.setItem(`${this.activePhase.id}_BANNER`, 1)); + }); + }, + startCountdown() { + this.activePhase.countdownRemaining < 0 || + (this.updateCountdown(), + this.activePhase.isCountdownStatic || + (this.countdownInterval = setInterval(() => { + ((this.activePhase.countdownRemaining -= 1e3), + this.activePhase.countdownRemaining > 0 + ? this.updateCountdown() + : this.close()); + }, 1e3))); + }, + updateCountdown() { + if (!this.isOpen) return; + const { countdownRemaining: n, showDaysAsHours: e } = this.activePhase, + t = { + days: Math.floor(n / 864e5), + hours: Math.floor((n % 864e5) / 36e5), + minutes: Math.floor((n % 36e5) / 6e4), + seconds: Math.floor((n % 6e4) / 1e3), + }; + e && ((t.hours = t.hours + 24 * t.days), delete t.days); + for (const [n, e] of Object.entries(t)) { + const t = document.querySelector(`[data-countdown=${n}]`); + t && (t.textContent = e.toString().padStart(2, "0")); + } + }, + init() { + if ( + !this.activePhase || + localStorage.getItem(this.activePhase.closedKey) || + this.isInitiated + ) + return console.info("Not initiating"); + ((this.isOpen = !0), (this.isInitiated = !0)); + const n = setInterval(() => { + "complete" === document.readyState && + (clearInterval(n), + (this.type = this.getType()), + this.insertStyleTag(), + this.render(), + this.bindCloseButton(), + this.activePhase.showCountdown && this.startCountdown()); + }, 100); + }, +}), + (window.BitterBrainsBanner.style = + "\n :root { --bb-logo-desktop: url(/banners/logo.svg);--bb-logo-tablet: url(/banners/mark.svg);--bb-logo-mobile: url(/banners/mark.svg); }#bb-banner {\n height: 72px;\n text-decoration: none;\n color: white;\n padding-right: 30px;\n display: flex;\n justify-content: center;\n align-items: center;\n background: #000;\n overflow: hidden;\n line-height: 1.25;\n background: var(--bb-gradient);\n direction: ltr !important;\n}\n\n#bb-banner .bb-logo {\n width: 30px;\n height: 19px;\n left: 16px;\n position: absolute;\n background-size: contain;\n background-repeat: no-repeat;\n background-image: var(--bb-logo-mobile);\n}\n\n#bb-banner .bb-close {\n font-family: sans-serif;\n color: white;\n position: absolute;\n top: 29px;\n right: 8px;\n line-height: 0;\n font-size: 24px;\n height: 14px;\n width: 14px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n#bb-banner .bb-background {\n background-image: var(--bb-bg-mobile);\n background-repeat: no-repeat;\n background-size: cover;\n background-position: center;\n position: absolute;\n z-index: 0;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n}\n\n#bb-banner .bb-core {\n display: flex;\n align-items: center;\n text-align: center;\n position: relative;\n z-index: 10;\n flex-direction: column;\n}\n\n#bb-banner .bb-core .bb-slogan {\n display: flex;\n flex-direction: row;\n align-items: center;\n order: 1;\n}\n\n#bb-banner .bb-core .bb-title-container {\n display: flex;\n align-items: center;\n flex-direction: column;\n gap: 2px;\n}\n\n#bb-banner .bb-core .bb-extended {\n display: none;\n}\n\n#bb-banner .bb-core .bb-title {\n font-size: 14px;\n font-weight: bold;\n color: #00dc82;\n}\n\n#bb-banner .bb-core .bb-subtitle {\n color: #FFF;\n font-size: 12px;\n}\n\n#bb-banner .bb-core .bb-button-wrapper {\n order: 2;\n}\n\n#bb-banner .bb-core .bb-button-wrapper .bb-button {\n border-radius: 6px;\n font-weight: bold;\n color: #000;\n background: white;\n font-size: 14px;\n padding: 14px 8px;\n white-space: nowrap;\n}\n\n#bb-banner .bb-core .bb-countdown-wrapper {\n display: flex;\n align-items: center;\n flex-direction: row-reverse;\n gap: 6px;\n order: 3;\n}\n\n#bb-banner .bb-core .bb-countdown-wrapper .bb-countdown-footer {\n order: 1;\n}\n\n#bb-banner .bb-core .bb-countdown {\n align-items: center;\n line-height: 1;\n display: flex;\n font-weight: bold;\n font-size: 12px;\n color: #00dc82;\n order: 2;\n}\n\n#bb-banner .bb-core .bb-countdown .bb-countdown-item {\n display: flex;\n align-items: center;\n}\n\n#bb-banner .bb-core .bb-countdown .bb-countdown-item .bb-countdown-part > div {\n display: inline;\n}\n\n#bb-banner .bb-core .bb-countdown .bb-countdown-item:last-child .bb-countdown-colon {\n display: none;\n}\n\n#bb-banner .bb-core .bb-countdown .bb-countdown-item .bb-countdown-part .bb-countdown-text .bb-countdown-text-initial~span {\n display: none;\n}\n\n\n@media screen and (max-width: 768px) {\n #bb-banner {\n padding-right: 0px !important;\n }\n}\n\n@media (min-width: 768px) {\n\n #bb-banner .bb-close {\n right: 12px;\n }\n\n #bb-banner .bb-core {\n gap: 12px;\n flex-direction: row;\n }\n\n #bb-banner .bb-background {\n background-image: var(--bb-bg-tablet);\n }\n\n #bb-banner .bb-logo {\n background-image: var(--bb-logo-tablet);\n }\n\n #bb-banner .bb-core .bb-title {\n font-size: 18px;\n }\n\n #bb-banner .bb-core .bb-subtitle {\n font-size: 16px;\n }\n\n #bb-banner .bb-core .bb-button-wrapper {\n display: block;\n }\n\n #bb-banner .bb-core .bb-button-wrapper .bb-button {\n font-size: 18px;\n }\n\n #bb-banner .bb-core .bb-countdown-wrapper {\n flex-direction: column-reverse;\n }\n}\n\n@media (min-width: 1280px) {\n #bb-banner .bb-core .bb-title-container {\n gap: 4px;\n }\n \n #bb-banner .bb-logo {\n width: 152px;\n left: 24px;\n background-image: var(--bb-logo-desktop);\n }\n\n #bb-banner .bb-core .bb-slogan {\n gap: 4px;\n }\n\n #bb-banner .bb-background {\n background-image: var(--bb-bg-desktop);\n }\n\n #bb-banner .bb-core {\n gap: 32px;\n }\n\n #bb-banner .bb-core .bb-countdown-wrapper {\n flex-direction: column-reverse;\n }\n}\n\nhtml.has-bb-banner.bb-banner-hidden #bb-banner {\n display: none;\n}\n\n/*\nBANNER TYPES\n*********************************************************/\n\n/* Banner rendered in container element */\n\nhtml.has-bb-banner #bb-banner-container #bb-banner, html.has-bb-banner #bb-banner-video #bb-banner {\n position: relative;\n}\n\nhtml.has-bb-banner #bb-banner-container #bb-banner .bb-close {\n/* display: none;*/\n}\n\n/* Banner Fixed Top */\n\nhtml.has-bb-banner.bb-type-top {\n margin-top: 72px;\n}\n\nhtml.has-bb-banner.bb-type-top.bb-banner-hidden {\n margin-top: 0;\n}\n\nhtml.has-bb-banner.bb-type-top #bb-banner {\n position: fixed;\n top: 0;\n z-index: 60;\n left: 0;\n right: 0;\n}\n\n/* Banner Fixed Bottom */\n\nhtml.has-bb-banner.bb-type-bottom #bb-banner {\n position: fixed;\n bottom: 10px;\n z-index: 60;\n left: 10px;\n right: 10px;\n}\n/* Vue School */\n\n\n#bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent {\n background: #100D1C;\n font-family: 'Inter', sans-serif;\n}\n\n\n#bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent:hover .bb-core .bb-button-wrapper .bb-button {\n background: #0EFAA5;\n color: #100D1C;\n transition: background-color 0.5s, color 0.5s; \n }\n\n#bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-logo {\n width: 122px;\n height: 40px;\n position: relative;\n display: block;\n background: url('/banners/logo.svg') !important;\n background-repeat: no-repeat;\n}\n\n\n\n#bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core {\n gap: 60px;\n transform: translateX(80px);\n\n}\n\n#bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-slogan .bb-title-container {\n display: flex;\n flex-direction: column;\n}\n\n\n#bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-slogan .bb-prelude {\ncolor: #FFF;\nfont-size: 24px;\nfont-style: normal;\nfont-weight: 900;\nline-height: normal; \nwidth: auto;\n \n}\n\n#bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-slogan .bb-prelude b {\n background: var(--Gradient, linear-gradient(90deg, #0EFAA5 1.04%, #FAFF00 100%));\n background-clip: text;\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent; \n font-style: normal;\n font-weight: 00;\n line-height: normal; \n}\n\n#bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-slogan {\n gap: 60px;\n}\n\n#bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-title {\n color: white;\ntext-align: center;\nfont-size: 16px;\nfont-style: normal;\nfont-weight: 400;\nline-height: normal; \n}\n\n#bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-title b {\n color: #0EFAA5;\n font-weight: 700;\n}\n\n#bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-subtitle {\n font-weight: 700;\n}\n\n\n#bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-countdown-container .bb-addendum {color: var(--White, #FFF);\n text-align: center;\n font-size: 10px;\n font-style: normal;\n font-weight: 700;\n line-height: normal;\n letter-spacing: 1px;\n text-transform: uppercase; \n}\n\n#bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-countdown-container {\n order: 3;\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n#bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-countdown-container .bb-countdown {\npadding: 6px 14px;\nborder-radius: 4px;\nbackground: rgba(43, 246, 163, 0.20);\ncolor: white;\ntext-align: center;\nfont-size: 18px;\nfont-style: normal;\nfont-weight: 800;\nline-height: normal;\nletter-spacing: 1.4px; \n}\n\n#bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-button-wrapper .bb-button {\n display: flex;\n color: white;\n align-items: center;\n justify-items: center;\n height: 44px;\n padding: 0px 24px;\n justify-content: center;\n align-items: center;\n gap: 10px;\n border-radius: 50px;\nborder: 2px solid #0EFAA5;\nbackground: rgba(14, 250, 165, 0.40);\nfont-size: 14px;\nfont-style: normal;\nfont-weight: 700;\nline-height: normal; \n}\n\n\n\n@media screen and (max-width: 1440px) {\n #bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core{\n gap: 30px;\n transform: translateX(0px);\n\n }\n\n #bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-countdown-container {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 2px;\n }\n\n #bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-logo {\n width: 50px;\n height: 40px;\n position: relative;\n display: block;\n background: url('/banners/mark.svg') !important;\n background-repeat: no-repeat !important;\n margin-right: 40px;\n }\n}\n@media screen and (max-width: 1280px) {\n #bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-slogan .bb-title-container {\n display: flex;\n flex-direction: column;\n }\n}\n\n@media screen and (max-width: 1234px) {\n #bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core{\n margin-right: 40px;\n }\n #bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-countdown-container {\n display: flex;\n flex-direction: row;\n justify-items: center;\n position: absolute;\n bottom: 0%;\n left: 50%;\n transform: translate(-35%, 46%);\n }\n\n #bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-countdown-container .bb-countdown {\n color: #0BD88F;\n padding: 0px;\n background: transparent;\ntext-align: center;\nfont-size: 12px;\nfont-style: normal;\nfont-weight: 700;\nline-height: 16px; /* 177.778% */\nletter-spacing: 0.9px; \nmargin-left: 2px;\n }\n\n #bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-slogan .bb-title-container {\n margin-top: -10px;\n }\n\n #bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-logo {\n width: 35px;\n height: 40px;\n position: relative;\n display: block;\n }\n\n #bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-slogan {\n gap: 20px;\n }\n\n #bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-slogan .bb-prelude {\n width: 160px;\n text-align: center;\n font-size: 20px;\n }\n\n #bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-slogan .bb-title-container .bb-title, #bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-slogan .bb-title-container .bb-subtitle {\n font-size: 14px !important;\n }\n}\n\n@media screen and (max-width: 768px) {\n #bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-countdown-container {\n position: relative;\n bottom: 0%;\n left: 0%;\n transform: translate(0%, 0%);\n }\n\n #bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-slogan {\n margin-top: 0px;\n gap: 0px;\n flex-direction: column;\n }\n\n #bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-slogan .bb-title-container {\n margin-top: 0px;\n gap: 0px;\n }\n\n #bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core {\n gap: 0px;\n flex-direction: column;\n \n }\n\n #bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-slogan .bb-title-container .bb-title, #bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-slogan .bb-title-container .bb-subtitle {\n font-size: 11px !important;\n }\n \n\n #bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-button-wrapper {\n display: none;\n }\n\n #bb-banner.bb-campaign-vs-affiliates-freeweekend-urgent .bb-core .bb-slogan .bb-prelude {\n font-size: 13px; \n width: auto;\n }\n}\n/* Vue Router and Pinia */\n\nhtml.has-bb-banner .theme,\nhtml.has-bb-banner .theme .nav-bar,\nhtml.has-bb-banner .theme .sidebar,\nhtml.has-bb-banner .theme .page {\n margin-top: 72px;\n}\n\n/* Vite Press */\n\nhtml.has-bb-banner {\n scroll-padding-top: 134px;\n overflow: auto;\n}\n\nhtml.has-bb-banner .VPSidebar,\nhtml.has-bb-banner .VPDoc.has-sidebar.has-aside .aside-container {\n top: 72px;\n}\n\n@media (min-width: 960px) {\n html.has-bb-banner .VPNav {\n top: 72px;\n }\n}\n\n ")); +try { + window.BitterBrainsBanner.init(); +} catch { + console.warn("Error when trying to render top banner"); +} diff --git a/DjangoAdmin_Tornado_EleVue/public/banners/logo2.svg b/DjangoAdmin_Tornado_EleVue/public/banners/logo2.svg new file mode 100644 index 0000000..737a290 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/public/banners/logo2.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DjangoAdmin_Tornado_EleVue/public/banners/mark.svg b/DjangoAdmin_Tornado_EleVue/public/banners/mark.svg new file mode 100644 index 0000000..7b6d683 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/public/banners/mark.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/1.png b/DjangoAdmin_Tornado_EleVue/public/demo/1.png new file mode 100644 index 0000000..d4816c0 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/1.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/10.png b/DjangoAdmin_Tornado_EleVue/public/demo/10.png new file mode 100644 index 0000000..14ce793 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/10.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/11.png b/DjangoAdmin_Tornado_EleVue/public/demo/11.png new file mode 100644 index 0000000..824d326 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/11.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/12.png b/DjangoAdmin_Tornado_EleVue/public/demo/12.png new file mode 100644 index 0000000..f36802e Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/12.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/13.png b/DjangoAdmin_Tornado_EleVue/public/demo/13.png new file mode 100644 index 0000000..a779ec6 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/13.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/14.png b/DjangoAdmin_Tornado_EleVue/public/demo/14.png new file mode 100644 index 0000000..6270bb5 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/14.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/15.png b/DjangoAdmin_Tornado_EleVue/public/demo/15.png new file mode 100644 index 0000000..dfc841f Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/15.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/16.png b/DjangoAdmin_Tornado_EleVue/public/demo/16.png new file mode 100644 index 0000000..a48ba54 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/16.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/17.png b/DjangoAdmin_Tornado_EleVue/public/demo/17.png new file mode 100644 index 0000000..9e6c1f8 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/17.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/18.png b/DjangoAdmin_Tornado_EleVue/public/demo/18.png new file mode 100644 index 0000000..e0f4754 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/18.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/19.png b/DjangoAdmin_Tornado_EleVue/public/demo/19.png new file mode 100644 index 0000000..571575e Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/19.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/2.png b/DjangoAdmin_Tornado_EleVue/public/demo/2.png new file mode 100644 index 0000000..e830420 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/2.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/20.png b/DjangoAdmin_Tornado_EleVue/public/demo/20.png new file mode 100644 index 0000000..c8f596e Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/20.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/21.png b/DjangoAdmin_Tornado_EleVue/public/demo/21.png new file mode 100644 index 0000000..dd71358 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/21.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/22.png b/DjangoAdmin_Tornado_EleVue/public/demo/22.png new file mode 100644 index 0000000..3a45f05 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/22.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/23.png b/DjangoAdmin_Tornado_EleVue/public/demo/23.png new file mode 100644 index 0000000..6d9b2b7 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/23.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/24.png b/DjangoAdmin_Tornado_EleVue/public/demo/24.png new file mode 100644 index 0000000..9d77bb6 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/24.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/25.png b/DjangoAdmin_Tornado_EleVue/public/demo/25.png new file mode 100644 index 0000000..77e82a4 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/25.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/26.png b/DjangoAdmin_Tornado_EleVue/public/demo/26.png new file mode 100644 index 0000000..7683fd1 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/26.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/27.png b/DjangoAdmin_Tornado_EleVue/public/demo/27.png new file mode 100644 index 0000000..5b89e79 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/27.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/28.png b/DjangoAdmin_Tornado_EleVue/public/demo/28.png new file mode 100644 index 0000000..b2a3a51 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/28.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/3.png b/DjangoAdmin_Tornado_EleVue/public/demo/3.png new file mode 100644 index 0000000..e00764d Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/3.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/4.png b/DjangoAdmin_Tornado_EleVue/public/demo/4.png new file mode 100644 index 0000000..272e672 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/4.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/5.png b/DjangoAdmin_Tornado_EleVue/public/demo/5.png new file mode 100644 index 0000000..0dc0649 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/5.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/6.png b/DjangoAdmin_Tornado_EleVue/public/demo/6.png new file mode 100644 index 0000000..2d861d4 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/6.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/7.png b/DjangoAdmin_Tornado_EleVue/public/demo/7.png new file mode 100644 index 0000000..1fefff4 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/7.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/8.png b/DjangoAdmin_Tornado_EleVue/public/demo/8.png new file mode 100644 index 0000000..76d24b9 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/8.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/demo/9.png b/DjangoAdmin_Tornado_EleVue/public/demo/9.png new file mode 100644 index 0000000..cfb626b Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/demo/9.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/logo.png b/DjangoAdmin_Tornado_EleVue/public/logo.png new file mode 100644 index 0000000..d9d2fdb Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/logo.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/logo.svg b/DjangoAdmin_Tornado_EleVue/public/logo.svg new file mode 100644 index 0000000..5ca2d2a --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/public/logo.svg @@ -0,0 +1,283 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DjangoAdmin_Tornado_EleVue/public/sponsor/djangoadmin.png b/DjangoAdmin_Tornado_EleVue/public/sponsor/djangoadmin.png new file mode 100644 index 0000000..cb13b29 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/sponsor/djangoadmin.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/sponsor/easygoadmin.png b/DjangoAdmin_Tornado_EleVue/public/sponsor/easygoadmin.png new file mode 100644 index 0000000..fb9be3c Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/sponsor/easygoadmin.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/sponsor/rxthinkcmf.png b/DjangoAdmin_Tornado_EleVue/public/sponsor/rxthinkcmf.png new file mode 100644 index 0000000..4397ad4 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/sponsor/rxthinkcmf.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/sponsor/xiaomayi.png b/DjangoAdmin_Tornado_EleVue/public/sponsor/xiaomayi.png new file mode 100644 index 0000000..83bf32d Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/sponsor/xiaomayi.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/volume1/1.png b/DjangoAdmin_Tornado_EleVue/public/volume1/1.png new file mode 100644 index 0000000..fdebd0a Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/volume1/1.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/volume1/database/1.png b/DjangoAdmin_Tornado_EleVue/public/volume1/database/1.png new file mode 100644 index 0000000..ba1da16 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/volume1/database/1.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/volume1/database/2.png b/DjangoAdmin_Tornado_EleVue/public/volume1/database/2.png new file mode 100644 index 0000000..9c06e15 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/volume1/database/2.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/volume1/environment/1.png b/DjangoAdmin_Tornado_EleVue/public/volume1/environment/1.png new file mode 100644 index 0000000..105065e Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/volume1/environment/1.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/volume1/environment/2.png b/DjangoAdmin_Tornado_EleVue/public/volume1/environment/2.png new file mode 100644 index 0000000..fa10267 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/volume1/environment/2.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/volume1/environment/3.png b/DjangoAdmin_Tornado_EleVue/public/volume1/environment/3.png new file mode 100644 index 0000000..60f10f9 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/volume1/environment/3.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/volume1/environment/4.png b/DjangoAdmin_Tornado_EleVue/public/volume1/environment/4.png new file mode 100644 index 0000000..bbd7ecd Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/volume1/environment/4.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/volume3/security/jwt_token.png b/DjangoAdmin_Tornado_EleVue/public/volume3/security/jwt_token.png new file mode 100644 index 0000000..84edc33 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/volume3/security/jwt_token.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/volume5/1.png b/DjangoAdmin_Tornado_EleVue/public/volume5/1.png new file mode 100644 index 0000000..5a72867 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/volume5/1.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/volume5/2.png b/DjangoAdmin_Tornado_EleVue/public/volume5/2.png new file mode 100644 index 0000000..6b58e8d Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/volume5/2.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/volume5/3.png b/DjangoAdmin_Tornado_EleVue/public/volume5/3.png new file mode 100644 index 0000000..ee4f2b2 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/volume5/3.png differ diff --git a/DjangoAdmin_Tornado_EleVue/public/volume5/4.png b/DjangoAdmin_Tornado_EleVue/public/volume5/4.png new file mode 100644 index 0000000..9692c62 Binary files /dev/null and b/DjangoAdmin_Tornado_EleVue/public/volume5/4.png differ diff --git a/DjangoAdmin_Tornado_EleVue/vite.config.ts b/DjangoAdmin_Tornado_EleVue/vite.config.ts new file mode 100644 index 0000000..021734c --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/vite.config.ts @@ -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"], + }, +}); diff --git a/DjangoAdmin_Tornado_EleVue/zh/changelog.md b/DjangoAdmin_Tornado_EleVue/zh/changelog.md new file mode 100644 index 0000000..fe570f2 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/changelog.md @@ -0,0 +1,308 @@ +# 更新日志 + +:::tip 说明 +更新日志是项目演进的完整记录,帮助开发者和企业了解每个版本的能力变化与技术演进方向。对于开发者,可快速掌握新功能与 API 变更,降低升级成本;对于企业,可评估版本稳定性与功能成熟度,为技术选型和升级决策提供依据。建议在升级前阅读目标版本的变更内容,确保业务代码平滑适配。 +::: + +## 版本总览 + +| 版本 | 日期 | 主要内容 | +|:-----|:-----|:---------| +| 2.0.0 | 2024-04-12 | Tornado 重构,Vue3 前后端分离奠基 | +| 2.1.0 | 2024-07-18 | RBAC 权限体系 + JWT 认证 | +| 2.2.0 | 2024-10-25 | 系统配置 + 字典增强 + 部门树 | +| 2.3.0 | 2025-01-08 | 内容管理 + XSS 防护 + 富文本 | +| 2.4.0 | 2025-03-22 | 多数据库 + SQLAlchemy 2.0 + Docker | +| 2.5.0 | 2025-06-15 | Vue3 升级 + TypeScript + Vite | +| 2.6.0 | 2025-09-03 | 代码生成器 + Excel 导入导出 | +| 2.7.0 | 2025-12-19 | 定时任务 + 操作日志 + 登录日志 | +| 2.8.0 | 2026-03-07 | 接口限流 + IP 地理 + 安全增强 | +| 2.9.0 | 2026-06-23 | 文档体系 + 契约测试 + 缓存优化 | +| **3.0.0** | **2026-09-01** | **架构重构:三层分离 + 基类体系** | + + +## [3.0.0] - 2026-09-01 + +**架构全面升级,奠定企业级框架基础。** + +### 新增 + +- **模块化分层架构**:业务模块按功能域分组(`system` / `tool` / `module`),职责清晰、可插拔扩展 +- **三层分离设计**:Endpoint(HTTP 接口层)→ Service(业务逻辑层)→ Repository(数据访问层),依赖单向传递,业务代码与框架核心完全解耦 +- **基类体系**:`BaseRepository` 提供 30+ 通用数据访问方法(CRUD、分页、软删除、条件查询);`BaseService` 封装唯一性校验、字段组装、序列化、缓存管理等通用业务逻辑。业务模块仅需 **4 个文件 + 1 行注册** 即可完成模块接入 +- **Handler 生命周期 + 装饰器**:CORS 跨域、JWT 认证、RBAC 权限、操作日志通过 Handler 生命周期方法和装饰器实现,职责单一,可插拔配置 +- **Repository 数据访问层全面落地**:业务代码与 SQLAlchemy ORM 彻底解耦,切换数据库或 ORM 框架对业务代码零影响 +- **契约测试体系**:路由快照、权限快照、表名快照三管齐下,自动守护接口契约,避免重构引发的接口漂移 +- **集成测试框架**:事务一致性验证 + 操作日志自动校验,确保核心业务流程稳定可靠 +- **跨库数据迁移工具**(`scripts/migrate_db.py`):支持 MySQL / PostgreSQL / SQL Server / SQLite / Oracle 间的全量或增量数据迁移,含表结构同步与数据泵送 +- **数据库版本化迁移**:基于 Alembic,Schema 变更可追溯、可回滚,支持多环境发布 +- **统一文件上传接口**:单一接口支持图片、文档、压缩包等全类型文件,流式校验,超限立即中断,避免内存溢出 +- **文件安全防护**:扩展名白名单 + 魔术字节(Magic Number)校验 + 路径遍历防护 + 高危格式拦截(如 `.php`、`.jsp` 等) + +### 优化 + +- 查询方法自动过滤软删除记录(`is_delete=True`),自动跳过空值查询条件,减少业务代码冗余 +- 批量删除统一响应格式,前后端文案保持一致 +- API 响应格式全面标准化(`code / msg / data`),前端解析零歧义 +- 文件上传流式大小校验,超限立即中断,避免大文件耗尽服务器内存 +- 配置项命名规范统一,遵循 `{模块}_{功能}_{属性}` 命名约定 +- 代码生成器模板同步适配三层架构,生成代码开箱即用 + +### 修复 + +- 上传接口参数缺失时未返回明确错误提示,现已补充完整参数校验与错误文案 +- Excel 导入时响应解析异常,修复数据类型映射逻辑 +- 数据表字段约束冲突,修复迁移脚本中字段定义重复问题 + + +## [2.9.0] - 2026-06-23 + +**文档体系与质量保障能力建设。** + +### 新增 + +- **企业级文档体系**:127 篇技术文档,覆盖快速入门、模块开发、架构设计、运维部署、规范标准全场景 +- **数据字典驱动**:字典编码自动映射显示文本,前端零配置,支持运行时刷新 +- **权限缓存主动失效**:权限变更实时生效,支持版本号兜底机制,极端场景下自动降级 +- **前端组件库文档化**:核心组件完整的 Props、Events、Slots 说明,含使用示例 +- **API 接口速查表**:按模块分组的接口清单,含路径、方法、权限标识、请求/响应示例 +- **多环境配置示例**:提供开发、测试、生产三套完整配置模板 + +### 优化 + +- 登录锁定策略:按 IP 与用户名组合统计失败次数,避免单一维度误锁 +- 会话管理:异常时自动回滚事务,防止数据库连接泄露 +- Token 续签:过期前 5 分钟自动刷新,用户无感知续期 +- 代码生成器:生成代码风格与手写代码一致,缩进与命名规范完全统一 + +### 修复 + +- 权限缓存刷新延迟,变更后最长 3 秒生效 +- Token 续签并发竞态,添加分布式锁保护 +- 代码生成器遗漏唯一性校验声明,现已自动生成 + + +## [2.8.0] - 2026-03-07 + +**安全防护与可观测性增强。** + +### 新增 + +- **IP 地理位置查询**:基于离线 GeoIP 数据库,服务启动时加载内存,查询零延迟,无外部 API 依赖 +- **User-Agent 解析**:自动识别客户端操作系统类型与版本、浏览器名称与版本 +- **接口限流**:基于 Redis 滑动窗口的按 IP 限流策略,支持按接口独立配置阈值,防刷防滥用 +- **操作日志自动记录**:所有写操作(POST / PUT / DELETE)自动捕获请求参数与响应结果,无需手动埋点 +- **验证码防刷**:按 IP 频率控制,单 IP 每分钟最多获取 5 次验证码,超限拒绝生成 + +### 优化 + +- JWT 认证增强:支持令牌主动刷新与黑名单机制,登出后令牌立即失效 +- 权限校验:管理员账号(`is_superuser=True`)自动放行所有权限校验 +- 会话管理:请求级事务自动提交与回滚,异常时自动回滚,无需手动处理 +- 文件上传安全:扩展名白名单与流式大小校验双重防护 + +### 修复 + +- 令牌刷新后旧令牌未加入黑名单,现已修复 +- 大文件上传超时,调整 Nginx 与 Tornado 超时参数 +- 并发请求会话泄露,修复会话绑定逻辑 + + +## [2.7.0] - 2025-12-19 + +**定时任务与日志审计能力上线。** + +### 新增 + +- **定时任务调度**:支持 Cron 表达式、固定间隔、指定时间三种触发方式,基于 APScheduler 实现 +- **任务执行日志**:完整记录每次执行的开始时间、结束时间、执行状态、耗时、错误详情 +- **登录日志**:记录所有登录与退出行为,含 IP 地址、地理位置、设备信息、登录结果 +- **数据字典缓存**:Redis 缓存加速字典查询,支持手动刷新与变更自动失效 +- **权限列表缓存**:变更时主动失效,保障权限数据实时性 + +### 优化 + +- 定时任务分布式锁:多实例部署时基于 Redis 分布式锁避免任务重复执行 +- 字典查询:支持按编码直接获取字典项列表,无需逐条查询 +- 系统配置:运行时动态修改,无需重启服务生效 + +### 修复 + +- 定时任务夏令时偏移,修复 Cron 表达式在夏令时切换日的执行偏差 +- 字典项排序不生效,修复排序字段映射错误 +- 配置值包含特殊字符时序列化异常,已修复 + + +## [2.6.0] - 2025-09-03 + +**代码生成能力上线,开发效率大幅提升。** + +### 新增 + +- **代码生成器(CLI)**:根据数据库表结构一键生成 Model、Repository、Service、Endpoint 及前端页面完整代码 +- **代码生成器(Web UI)**:可视化操作界面,支持单表生成与批量生成 +- **自定义模板引擎**:基于 Jinja2,支持自定义生成代码风格与目录结构 +- **Excel 导入导出**:支持模板下载、数据校验、错误行跳过与错误详情导出 +- **文件模板管理**:支持模板文件上传、版本管理与在线编辑 + +### 优化 + +- 代码生成器自动注册路由与权限节点,生成即用 +- 生成代码自动适配分层架构(Endpoint / Service / Repository) +- Excel 导入数据校验增强:支持必填、唯一性、长度、正则表达式等多种校验规则 + +### 修复 + +- 生成代码唯一性校验遗漏字段,已补全 +- Excel 导出日期格式不一致(如 `2025-09-01` 与 `09/01/2025`),现已统一为 `YYYY-MM-DD HH:mm:ss` +- 模板渲染中文乱码,统一文件编码为 UTF-8 + + +## [2.5.0] - 2025-06-15 + +**前端技术栈全面升级至 Vue3 生态。** + +### 新增 + +- **Vue3 升级**:前端框架从 Vue 2 升级至 Vue3.4+,全面拥抱 Composition API +- **ElementPlus**:UI 组件库适配 Vue3 生态 +- **Pinia 状态管理**:替代 Vuex,更轻量、更灵活、TypeScript 支持更完善 +- **TypeScript 类型安全**:全量类型定义,IDE 智能提示与编译时类型检查 +- **Vite 构建工具**:替代 Webpack,开发服务器毫秒级启动,HMR 即时生效 + +### 优化 + +- 构建速度:Vite 开发服务器启动时间 < 500ms,热更新 < 100ms +- 代码规范:ESLint + Prettier + Stylelint 统一代码风格 +- 组件体系:可插拔架构重构,按需加载,首屏加载体积减少 40% + +### 修复 + +- 热更新偶尔失效,修复 HMR 边界条件 +- TypeScript 类型定义不完整,补全 200+ 类型声明 +- 部分组件样式在 Vue3 下兼容性问题,已修复 + + +## [2.4.0] - 2025-03-22 + +**多数据库与企业级部署能力增强。** + +### 新增 + +- **多数据库支持**:通过 `DB_DRIVER` 环境变量一键切换 MySQL / PostgreSQL / SQL Server / SQLite / Oracle,业务代码零改动 +- **SQLAlchemy 2.0**:升级至 SQLAlchemy 2.0+,声明式映射,统一 Core 与 ORM 接口 +- **Pydantic v2**:校验性能提升 5-10 倍(Rust 核心重写),类型提示更丰富 +- **Docker 容器化**:提供 Dockerfile + docker-compose.yml,支持一键启动完整环境 + +### 优化 + +- 数据库连接池参数可调(`POOL_SIZE` / `MAX_OVERFLOW` / `POOL_TIMEOUT`) +- 多数据库方言自动适配,分页语法差异由框架层屏蔽 +- Docker 多阶段构建,最终镜像体积减小 60% + +### 修复 + +- PostgreSQL 日期时间类型与 Python datetime 映射兼容 +- SQL Server 分页查询使用 `OFFSET FETCH` 语法适配 +- Oracle 大文本字段(CLOB)序列化异常,已修复 + + +## [2.3.0] - 2025-01-08 + +**内容管理与 XSS 安全防护。** + +### 新增 + +- **内容管理模块**:文章管理(分类、标签、状态流转)、公告管理(置顶、有效期)、友情链接、城市级联数据 +- **富文本编辑器**:基于 WangEditor 集成,支持图片上传、视频嵌入、表格等丰富功能 +- **XSS 防护**:基于 bleach 库的白名单 HTML 清洗机制,富文本内容自动过滤恶意脚本 +- **文件字段自动处理**:上传文件自动迁移至目标存储,URL 自动补全 +- **富文本字段自动处理**:嵌入图片自动迁移存储,内容自动 XSS 清洗 + +### 优化 + +- 文章状态流转:草稿 → 审核 → 发布 → 下架,完整生命周期管理 +- 公告功能:支持置顶排序与有效期范围控制 +- 文件上传:支持图片预览与裁剪功能 + +### 修复 + +- 富文本编辑器中图片路径为相对路径导致无法访问,已修复为绝对 URL +- 编辑时临时文件未清理,现已自动清理 +- 批量删除时关联数据未同步删除,添加级联处理 + + +## [2.2.0] - 2024-10-25 + +**系统配置与基础数据管理增强。** + +### 新增 + +- **系统配置模块**:动态配置参数,运行时可调,修改即时生效,无需重启 +- **参数管理模块**:系统参数与业务参数分类管理,支持参数分组与权限控制 +- **字典模块增强**:字典项排序、启用/停用状态控制、批量导入导出 +- **部门树形结构**:无限级部门树,支持拖拽排序与层级校验 + +### 优化 + +- 配置查询:支持按配置编码直接获取值,支持默认值兜底 +- 字典导入导出:支持 Excel 批量导入字典项,含数据校验 +- 部门排序:拖拽交互优化,实时保存排序结果 + +### 修复 + +- 字典编码重复校验遗漏,新增唯一性校验 +- 部门删除时子部门未同步处理,添加级联删除选项 +- 空配置值序列化异常,统一返回空字符串 + + +## [2.1.0] - 2024-07-18 + +**RBAC 权限体系与认证安全上线。** + +### 新增 + +- **RBAC 权限体系**:用户-角色-菜单三层权限模型,支持按钮级权限控制 +- **JWT 认证**:无状态令牌认证,支持令牌刷新与黑名单机制 +- **登录安全**:图形验证码、失败次数锁定(5 次失败锁定 15 分钟)、密码 bcrypt 加密存储 +- **菜单管理**:动态路由生成,权限节点可视化配置 +- **权限装饰器**:后端接口级权限拦截(`@permission_required`) +- **权限指令**:前端按钮级权限控制(`v-permission`) + +### 优化 + +- 管理员账号(`is_superuser=True`)自动放行所有权限校验 +- 权限数据缓存加速,减少数据库查询 +- 令牌过期自动跳转登录页,携带 redirect URL + +### 修复 + +- 令牌过期响应码统一为 401,前端统一拦截处理 +- 权限缓存刷新延迟,变更后实时失效 +- 登录锁定时间计算错误,修复时间窗口逻辑 + + +## [2.0.0] - 2024-04-12 + +**Tornado 重构,前后端分离架构奠基。** + +### 新增 + +- **Tornado 异步框架**:原生支持 async/await,轻量灵活,稳定可靠 +- **SQLAlchemy ORM**:声明式模型定义,多数据库方言支持 +- **Pydantic 数据校验**:声明式 Schema 校验,自动生成 API 文档与校验逻辑 +- **Swagger 接口文档**:内置 Swagger UI 与 ReDoc,交互式 API 调试 +- **Redis 异步客户端**:基于 redis-py + hiredis,用于缓存与会话管理 +- **用户管理**:用户账号管理、密码重置、启用/停用状态控制 +- **角色管理**:角色 CRUD、权限节点配置 +- **菜单管理**:菜单 CRUD、图标设置、动态路由生成 +- **部门管理**:部门树形结构管理 +- **岗位管理**:岗位信息维护 +- **职级管理**:职级信息维护 + +### 架构特性 + +- 前后端完全分离架构 +- RESTful API 设计规范 +- 统一响应格式与异常处理 +- 异步请求全链路支持 \ No newline at end of file diff --git a/DjangoAdmin_Tornado_EleVue/zh/course.md b/DjangoAdmin_Tornado_EleVue/zh/course.md new file mode 100644 index 0000000..59e63d2 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/course.md @@ -0,0 +1,399 @@ +:::warning 特别提醒 +官方精心制作本教程,目的在于方便用户快速掌握软件产品的使用和部署。通过此教程,刚入行的开发者也可以快速掌握并投入产品研发。本地部署时务必请耐心阅读文档操作。 +::: + +## 教程概述 + +本教程将带你从零开始,完成一个完整的后台管理系统的搭建和运行。整个过程分为以下几个步骤: + +``` +1. 环境准备:安装 Python、MySQL、Redis、Node.js 等基础软件。 +2. 获取源码:从官方网站下载授权源码包并解压。 +3. 后端启动:安装 Python 依赖、配置环境变量、初始化数据库、启动后端服务。 +4. 前端启动:安装前端依赖、启动前端开发服务器。 +5. 功能验证:登录系统,验证各功能模块是否正常。 +6. 新增模块:以岗位管理为例,演示如何新增一个完整的业务模块。 +``` + +## 第一步:环境准备 + +确保电脑上已安装以下软件: + +| 软件 | 版本要求 | 用途 | +|------|----------|------| +| Python | 3.12+ | 后端运行环境 | +| MySQL | 8.0+ | 数据库 | +| Redis | 3.2+ | 缓存(验证码、JWT黑名单、限流) | +| Node.js | 22+ | 前端构建环境 | +| pnpm | 最新版 | 前端包管理器 | +| Git | 最新版 | 版本控制 | + +:::tip 温馨提示 +详细的安装步骤请参考 [环境准备](/zh/volume1/environment) 章节。 +::: + +## 第二步:获取源码 + +前往 [官方网站](https://www.djangoadmin.cn) 购买授权后,按以下步骤获取源码: + +``` +1. 登录官方网站,进入「个人中心」→「我的订单」页面。 +2. 在订单列表中找到已购买的授权订单,点击「下载」按钮。 +3. 下载的压缩包包含完整的前后端源码、数据库脚本及部署配置文件。 +4. 将压缩包解压到本地开发目录(如 E:\Projects\ 或 ~/Projects/)。 +``` + +```bash +# 进入项目目录(以实际解压路径为准) +cd djangoadmin +``` + +:::tip 温馨提示 +1. 源码包请务必从官方网站订单中心下载,确保获取的是正版授权的最新版本。 +2. 授权有效期内可无限次下载最新版本,版本更新后可重新下载获取最新源码。 +3. 解压后请先阅读根目录下的 `README.md` 和 `CHANGELOG.md` 了解版本更新内容。 +::: + +## 第三步:后端启动 + +### 3.1 安装 Python 依赖 + +```bash +# 创建虚拟环境(推荐) +python -m venv venv + +# 激活虚拟环境 +# Windows +venv\Scripts\activate +# Linux/macOS +source venv/bin/activate + +# 安装依赖 +pip install -r requirements.txt +``` + +### 3.2 配置环境变量 + +```bash +# 复制环境变量模板 +cp .env.example .env +``` + +编辑 `.env` 文件,配置数据库和 Redis 连接信息: + +```bash +# 数据库配置 +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=djangoadmin.tornado.elevue2 +DB_USERNAME=root +DB_PASSWORD=your_password + +# Redis 配置 +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 +REDIS_PASSWORD=your_redis_password + +# JWT 密钥(至少32字节) +JWT_SECRET=your_random_secret_key_at_least_32_bytes_long +``` + +:::danger +`JWT_SECRET` 必须配置,留空时应用会拒绝启动。生成方式: +```bash +python -c "import secrets; print(secrets.token_urlsafe(48))" +``` +::: + +### 3.3 初始化数据库 + +**方式一:手动导入(推荐)** + +```bash +# 1. 登录 MySQL,创建数据库 +mysql -uroot -p +CREATE DATABASE `djangoadmin.tornado.elevue2` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + +# 2. 导入数据库脚本(脚本位于 document/mysql/ 目录下) +mysql -uroot -p djangoadmin.tornado.elevue2 < document/mysql/djangoadmin.tornado.elevue2.sql +``` + +:::tip 温馨提示 +项目支持 MySQL、PostgreSQL、SQL Server、SQLite、Oracle 等数据库,对应的 SQL 脚本分别存放在 `document/` 目录下的对应文件夹中(`mysql/`、`postgresql/`、`sqlserver/`、`sqlite/`、`oracle/`)。请根据你使用的数据库类型选择对应的脚本导入。详细的多数据库初始化步骤请参考 [数据库初始化](/zh/volume1/database) 章节。 +::: + +**方式二:脚本初始化(备选)** + +```bash +# 自动完成建库、建表、导入种子数据 +python scripts/init_db.py +``` + +:::tip 温馨提示 +以上命令是推荐的直接执行方式,适用于所有操作系统。文档中出现的 `make` 命令(如 `make db-init`)是 Linux / macOS 系统的便捷别名,Windows 用户直接使用对应的原始命令即可(如 `python scripts/init_db.py`)。 +::: + +初始化完成后,数据库中包含以下默认数据: + +``` +管理员账号:admin / 123456(超级管理员,ID=1,跳过权限校验) +默认角色:超级管理员角色 +默认菜单:系统管理、内容管理、监控管理等菜单及权限节点 +数据字典:系统内置字典数据 +系统配置:系统默认配置项 +``` + +### 3.4 启动后端服务 + +```bash +python app.py +``` + +启动成功后会看到: + +``` +INFO: Tornado 服务启动成功 on http://127.0.0.1:8041 (Press CTRL+C to quit) +INFO: Application startup complete. +``` + +:::tip 温馨提示 +打开浏览器访问 `http://127.0.0.1:8041/docs` 可查看 Swagger 交互式 API 文档。 +::: + +## 第四步:前端启动 + +### 4.1 安装前端依赖 + +```bash +# 进入前端目录 +cd ui + +# 安装依赖 +pnpm install +``` + +:::tip 温馨提示 +如果安装速度较慢,可配置国内镜像源: +```bash +pnpm config set registry https://registry.npmmirror.com +``` +::: + +### 4.2 启动前端开发服务器 + +```bash +pnpm dev +``` + +启动成功后会看到: + +``` + VITE v3.x.x ready in xxx ms + + ➜ Local: http://localhost:8001/ +``` + +### 4.3 访问系统 + +打开浏览器访问 `http://localhost:8001`,使用默认账号登录: + +``` +账号:admin +密码:123456 +``` + +:::tip 温馨提示 +前端默认运行在 `8001` 端口,后端默认 `8041` 端口。`vite.config.ts` 中配置了代理,前端请求 `/api/*` 路径时自动转发到后端。 +::: + +## 第五步:功能验证 + +登录成功后,可以验证以下核心功能: + +``` +1. 控制台:首页仪表盘,显示系统概况。 +2. 用户管理:系统管理 → 用户管理,查看用户列表、新增、编辑、删除。 +3. 角色管理:系统管理 → 角色管理,管理角色和权限分配。 +4. 菜单管理:系统管理 → 菜单管理,管理菜单和权限节点。 +5. 数据字典:数据管理 → 字典管理,管理系统字典数据。 +6. 操作日志:系统管理 → 日志管理 → 操作日志,查看操作记录。 +``` + +## 第六步:新增业务模块 + +以「岗位管理」为例,演示如何新增一个完整的 CRUD 模块。 + +### 6.1 后端开发 + +在 `src/modules/position/` 目录下创建以下文件: + +**models.py** — 定义数据模型: + +```python +# +====================================================================== +# | 模块: 岗位数据模型层 +# | 说明: 定义岗位表结构,继承 BaseModel 获得通用字段与序列化能力 +# +====================================================================== +from sqlalchemy import Column, String, Integer, text +from config.database import DB_PREFIX +from core.base_model import BaseModel + +class Position(BaseModel): + """岗位模型""" + __tablename__ = DB_PREFIX + "position" + __table_args__ = {"comment": "岗位表"} + + # 岗位名称 + name = Column(String(255), nullable=False, index=True, comment="岗位名称") + # 岗位状态:0-正常 1-停用 + status = Column(Integer, default=0, server_default=text("0"), comment="岗位状态") + # 岗位排序 + sort = Column(Integer, default=0, server_default=text("0"), comment="岗位排序") +``` + +**schemas.py** — 定义表单校验: + +```python +# +====================================================================== +# | 模块: 岗位请求校验模型层 +# | 说明: 定义岗位创建/编辑与状态更新的 Pydantic 校验模型 +# +====================================================================== +from pydantic import Field +from core.base_schema import BaseSchema, BaseStatusSchema + +class PositionSchema(BaseSchema): + """岗位创建/编辑表单""" + name: str = Field(..., min_length=1, max_length=150, description="岗位名称") + status: int = Field(default=0, ge=0, le=1, description="岗位状态") + sort: int = Field(default=0, ge=0, le=99999, description="岗位排序") + +class PositionStatusSchema(BaseStatusSchema): + """岗位状态更新表单""" + pass +``` + +**repository.py** — 定义数据访问层: + +```python +# +====================================================================== +# | 模块: 岗位数据访问层 +# | 说明: 定义岗位仓储,继承 BaseRepository 获得通用 CRUD 与软删除能力 +# +====================================================================== +from core.base_repository import BaseRepository +from modules.position.models import Position + +class PositionRepository(BaseRepository): + """岗位数据仓库""" + model = Position +``` + +**service.py** — 定义业务逻辑层: + +```python +# +====================================================================== +# | 模块: 岗位业务逻辑层 +# | 说明: 定义岗位服务,声明差异点,最后导出服务单例 +# +====================================================================== +from core.base_service import BaseService +from modules.position.models import Position +from modules.position.repository import PositionRepository +from modules.position.schemas import PositionSchema, PositionStatusSchema + +class PositionService(BaseService): + """岗位服务""" + # 基础声明 + model = Position + repo_cls = PositionRepository + create_schema = PositionSchema + update_schema = PositionSchema + status_schema = PositionStatusSchema + # 分页查询字段与排序 + page_like_fields = ("name",) + page_eq_fields = ("status",) + page_order_by = (("sort", "asc"),) + # 唯一性校验 + unique_fields = {"name": "岗位名称不能重复"} + +# 服务单例 +position_service = PositionService() +``` + +### 6.2 注册路由 + +在 `src/api/v1/position.py` 中定义路由,然后在 `src/api/v1/router.py` 中注册: + +```python +# src/api/v1/position.py +from modules.position.handlers import ( + PositionPageHandler, PositionDetailHandler, PositionAddHandler, + PositionUpdateHandler, PositionDeleteHandler, PositionStatusHandler, + PositionBatchDeleteHandler, +) + +routes = [ + (r"/position/page", PositionPageHandler), + (r"/position/detail/(\d+)", PositionDetailHandler), + (r"/position/add", PositionAddHandler), + (r"/position/update", PositionUpdateHandler), + (r"/position/delete/(\d+)", PositionDeleteHandler), + (r"/position/status", PositionStatusHandler), + (r"/position/batchDelete", PositionBatchDeleteHandler), +] +``` + +```python +# src/api/v1/router.py — 在 _RESOURCE_MODULES 列表中追加 +from api.v1 import position +_RESOURCE_MODULES = [ + # ... 已有模块 ... + position, +] +``` + +### 6.3 前端开发 + +在 `ui/src/api/system/position.ts` 中定义接口,在 `ui/src/views/system/position/` 中创建页面文件(index.vue、edit.vue、columns.ts、querySchemas.ts)。 + +### 6.4 配置菜单权限 + +在系统管理 → 菜单管理中: + +``` +1. 新增菜单:岗位管理(路径:/system/position,组件:system/position/index) +2. 新增按钮:查看(sys:position:page)、新增(sys:position:add)、编辑(sys:position:update)、删除(sys:position:delete) +3. 在角色管理中为角色分配"岗位管理"菜单权限 +``` + +:::tip 温馨提示 +详细的端到端教程请参考 [模块开发实战](/zh/volume2/overview) 章节。 +::: + +## 常见问题 + +在部署和使用过程中,可能会遇到以下常见问题: + +``` +1. 端口被占用:修改 .env 中的 TORNADO_PORT 或终止占用端口的进程。 +2. 数据库连接失败:检查 .env 中的数据库配置,确认 MySQL 服务已启动。 +3. Redis 连接失败:检查 Redis 服务状态和密码配置。 +4. JWT_SECRET 未配置:使用 python -c "import secrets; print(secrets.token_urlsafe(48))" 生成。 +5. pnpm install 失败:配置国内镜像 pnpm config set registry https://registry.npmmirror.com。 +``` + +:::tip 温馨提示 +更多问题请参考 [常见问题FAQ](/zh/volume1/faq) 章节。 +::: + +## 总结 + +通过以上步骤,你已经完成了从零搭建一个完整的后台管理系统的全过程: + +``` +1. 环境准备:Python + MySQL + Redis + Node.js + pnpm +2. 后端启动:pip install → 配置 .env → 初始化数据库 → python app.py +3. 前端启动:pnpm install → pnpm dev +4. 功能验证:登录系统,验证各模块功能 +5. 新增模块:models → schemas → repository → service → endpoint → 前端页面 → 菜单权限 +``` + +整个搭建过程约 30 分钟即可完成。如果在操作过程中遇到问题,请查阅文档或在社区寻求帮助。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/faq.md b/DjangoAdmin_Tornado_EleVue/zh/faq.md new file mode 100644 index 0000000..e5299d3 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/faq.md @@ -0,0 +1,85 @@ +# 常见问题汇总 + +:::tip 说明 +本页汇总了项目各模块的常见问题及解决方案,按场景分类索引。遇到问题时可先查阅本页,再深入各章节详细文档。 +::: + +## 快速入门 + +| 问题 | 说明 | 详细文档 | +|------|------|----------| +| 端口被占用 | 8041 或 8001 端口被其他进程占用 | [快速入门 FAQ](/zh/volume1/faq) | +| 数据库连接失败 | MySQL 未启动或配置错误 | [快速入门 FAQ](/zh/volume1/faq) | +| Redis 连接失败 | Redis 未启动或密码配置错误 | [快速入门 FAQ](/zh/volume1/faq) | +| pip install 安装失败 | 网络问题或依赖冲突 | [快速入门 FAQ](/zh/volume1/faq) | +| JWT_SECRET 未配置 | 环境变量缺失 | [快速入门 FAQ](/zh/volume1/faq) | +| pnpm install 安装失败 | Node.js 版本或网络问题 | [快速入门 FAQ](/zh/volume1/faq) | +| API 请求返回 404 | 路由未注册或代理配置错误 | [快速入门 FAQ](/zh/volume1/faq) | +| 验证码图片不显示 | Pillow 依赖或字体问题 | [快速入门 FAQ](/zh/volume1/faq) | +| 接口返回 403 权限不足 | 权限节点未配置 | [快速入门 FAQ](/zh/volume1/faq) | +| 文件上传失败 | 扩展名不在白名单或大小超限 | [快速入门 FAQ](/zh/volume1/faq) | + +## 后端开发 + +| 问题 | 说明 | 详细文档 | +|------|------|----------| +| 字段类型不匹配 | 模型字段与 Schema 类型不一致 | [模块开发 FAQ](/zh/volume2/faq) | +| 唯一性校验遗漏 | 未配置 unique_fields | [模块开发 FAQ](/zh/volume2/faq) | +| 软删除过滤缺失 | filter() 不自动过滤软删除 | [模块开发 FAQ](/zh/volume2/faq) | +| 分页参数命名不一致 | pageNo/pageSize 命名规范 | [分页规范](/zh/volume7/pagination) | +| 权限节点未配置 | 后端装饰器与前端指令不匹配 | [权限节点联动](/zh/volume4/integration/perm-sync) | + +## 前端开发 + +| 问题 | 说明 | 详细文档 | +|------|------|----------| +| 开发环境 API 请求 404 | VITE_PROXY 配置错误 | [前端构建](/zh/volume4/frontend/build) | +| 构建后页面空白 | VITE_PUBLIC_PATH 不一致 | [前端构建](/zh/volume4/frontend/build) | +| 环境变量不生效 | 变量名未以 VITE_ 开头 | [前端构建](/zh/volume4/frontend/build) | +| 热更新不工作 | 修改了 vite.config.ts 需重启 | [前端构建](/zh/volume4/frontend/build) | +| 上传组件不显示 | FLASK_FILE_URL 配置错误 | [文件上传联调](/zh/volume4/integration/upload-sync) | + +## 运维部署 + +| 问题 | 说明 | 详细文档 | +|------|------|----------| +| 502 Bad Gateway | 后端服务未启动 | [Nginx 反向代理](/zh/volume6/nginx) | +| 413 Request Entity Too Large | 上传文件超过限制 | [Nginx 反向代理](/zh/volume6/nginx) | +| 刷新页面 404 | 缺少 try_files 配置 | [Nginx 反向代理](/zh/volume6/nginx) | +| Supervisor 启动失败 | 配置文件路径错误 | [Supervisor](/zh/volume6/supervisor) | +| Docker 容器启动失败 | 环境变量或网络配置错误 | [Docker 容器化](/zh/volume6/docker) | +| 数据库迁移失败 | 数据类型不兼容 | [数据库迁移](/zh/volume6/db-migrate) | + +## 代码生成器 + +| 问题 | 说明 | 详细文档 | +|------|------|----------| +| 模块已存在 | 目标目录已有同名模块 | [代码生成器 FAQ](/zh/volume5/faq) | +| 生成代码报错 | 模板语法或配置问题 | [代码生成器 FAQ](/zh/volume5/faq) | +| 字段识别不准确 | 注释或类型推断问题 | [代码生成器 FAQ](/zh/volume5/faq) | + +## 工具模块 + +| 问题 | 说明 | 详细文档 | +|------|------|----------| +| IP 地理位置显示"未知" | xdb 文件不存在或 IP 格式问题 | [IP/UA 解析](/zh/volume4/tools/ip-ua) | +| 限流不生效 | RATE_LIMIT_ENABLED 未开启 | [IP/UA 解析](/zh/volume4/tools/ip-ua) | +| 富文本 XSS 被过滤 | bleach 清洗规则过严 | [富文本过滤](/zh/volume4/tools/rich-text) | +| 文件上传被拒绝 | 扩展名不在白名单 | [上传配置](/zh/volume4/tools/upload-guide) | + +## 排查思路 + +遇到问题时,按以下顺序排查: + +``` +1. 查看后端日志(终端输出或 logs/ 目录) +2. 查看浏览器 Network 面板(请求 URL、状态码、响应体) +3. 查看 Swagger 文档(http://127.0.0.1:8041/docs) +4. 检查 .env 配置(环境变量是否正确) +5. 检查 Redis/MySQL 连接(服务是否启动) +6. 查阅对应模块的详细文档 +``` + +## 总结 + +常见问题主要集中在环境配置、权限节点、文件上传、部署配置四个方面。遇到问题时优先查看后端日志和浏览器 Network 面板,大部分问题可通过检查 `.env` 配置和 Swagger 文档定位原因。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/introduction.md b/DjangoAdmin_Tornado_EleVue/zh/introduction.md new file mode 100644 index 0000000..947ec5a --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/introduction.md @@ -0,0 +1,157 @@ +
+

+ logo +

+
+ +

企业级数据中台解决方案

+ + + +## 软件说明 + +基于 Tornado、SQLAlchemy、Pydantic、Vue3、TypeScript、Vite、ElementPlus、MySQL 等技术栈构建的企业级前后端分离后台管理系统。 + +后端采用 Python 语言,基于 Tornado 异步框架、SQLAlchemy ORM、Pydantic v2 数据校验等主流技术栈;前端基于 Vue3、TypeScript、Vite 构建,采用 ElementPlus 框架,遵循 Vue 前后端分离主流设计思想。 + +系统采用完全前后端分离架构,前端通过 API 调用后端服务实现数据交互。内置完善的 RBAC 权限体系,权限颗粒度精细至按钮级别;支持多主题切换,兼容桌面端和平板端。系统预置了用户、角色、菜单、部门、岗位、职级、日志、字典、配置、参数、定时任务、代码生成器等基础功能模块,开箱即用。 + +技术栈特性: + ++ **异步原生**:基于 Tornado 异步框架,请求处理不阻塞事件循环,性能优异 ++ **类型安全**:Pydantic v2 强类型校验,运行时自动验证请求参数 ++ **分层架构**:Handler(HTTP)→ Service(业务)→ Repository(数据)→ Model(ORM),职责清晰 ++ **基类复用**:BaseRepository / BaseService 提供 30+ 通用方法,子类只需声明差异 ++ **模块可插拔**:业务模块位于 `src/modules/`,按需增删不影响核心框架 ++ **多数据库支持**:通过 `DB_DRIVER` 环境变量一键切换 MySQL / PostgreSQL / SQL Server / SQLite / Oracle ++ **装饰器可扩展**:CORS、认证、权限、日志通过装饰器实现,按需组合 ++ **RBAC 权限**:后端装饰器 + 前端指令双重校验,精确到按钮级 ++ **代码生成器**:根据数据库表结构一键生成前后端 CRUD 代码 + +## 集成模块 + ++ **用户管理**:实现用户数据录入与管理,支持密码强度校验、头像裁剪 ++ **角色管理**:基于 RBAC 权限架构体系,实现角色权限的管理 ++ **菜单管理**:用于权限菜单数据管理以及菜单节点权限数据管理 ++ **部门管理**:用于管理系统架构体系中的部门数据 ++ **岗位管理**:针对用户岗位属性的数据进行统一维护管理 ++ **职级管理**:针对用户职级属性的数据进行统一维护管理,支持 Excel 导入导出 ++ **日志管理**:用于登录日志、操作日志管理和维护,中间件自动记录 ++ **参数管理**:用于对系统核心参数的定义和管理 ++ **配置管理**:用于对动态配置参数的统一管理和设置 ++ **字典管理**:用于对字典数据、字典项数据的管理和维护 ++ **通知公告**:主要对通知、公告等站内数据进行管理 ++ **行政区划**:针对全国行政区划分数据进行管理和维护 ++ **文件模板**:主要对系统中使用的文件模板进行上传和管理 ++ **任务调度**:主要用于对定时任务进行定义和统一调度管理 ++ **代码生成**:根据数据库表结构一键生成前后端 CRUD 代码,含列表、表单、API 全套 ++ **接口文档**:用于查阅系统接口在线文档,查阅 API 地址、入参、出参等 + +## 软件出处 + ++ 软件名称:小蚂蚁云前后端分离Tornado+ElementPlus版本 ++ 软件作者:@小蚂蚁云团队 ++ 软件协议:Apache-2.0 ++ 官网网址:[https://www.djangoadmin.cn](https://www.djangoadmin.cn) ++ 文档网址:[http://docs.tornado.elevue2.djangoadmin.cn](http://docs.tornado.elevue2.djangoadmin.cn) ++ 演示地址:[http://manage.tornado.elevue2.djangoadmin.cn](http://manage.tornado.elevue2.djangoadmin.cn) + +## 软件版本 + +| 版本名称 | 说明 | 地址 | +|:---------|:-----|:-----| +| Django+Layui混编版 | 基于 Django、Jinja2、Layui 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Django_Layui) | +| Django+ElementPlus前后端分离版 | 基于 Django、Django ORM、Vue3、ElementPlus、TypeScript、Vite 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Django_EleVue) | +| Django+AntDesign前后端分离版 | 基于 Django、Django ORM、Vue3、AntDesign、TypeScript、Vite 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Django_AntdVue) | +| Django+NaiveUI前后端分离版 | 基于 Django、Django ORM、Vue3、NaiveUI、TypeScript、Vite 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Django_NaiveVue) | +| Flask+Layui混编版 | 基于 Flask、Jinja2、Layui 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Flask_Layui) | +| Flask+ElementPlus前后端分离版 | 基于 Flask、SQLAlchemy、WTForms、Vue3、ElementPlus、TypeScript、Vite 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Flask_EleVue) | +| Flask+AntDesign前后端分离版 | 基于 Flask、SQLAlchemy、WTForms、Vue3、AntDesign、TypeScript、Vite 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Flask_AntdVue) | +| Flask+NaiveUI前后端分离版 | 基于 Flask、SQLAlchemy、WTForms、Vue3、NaiveUI、TypeScript、Vite 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Flask_NaiveVue) | +| Tornado+Layui混编版 | 基于 Tornado、Jinja2、Layui 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Fastapi_Layui) | +| Tornado+ElementPlus前后端分离版 | 基于 Tornado、SQLAlchemy、Pydantic、Vue3、ElementPlus、TypeScript、Vite 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Fastapi_EleVue) | +| Tornado+AntDesign前后端分离版 | 基于 Tornado、SQLAlchemy、Pydantic、Vue3、AntDesign、TypeScript、Vite 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Fastapi_AntdVue) | +| Tornado+NaiveUI前后端分离版 | 基于 Tornado、SQLAlchemy、Pydantic、Vue3、NaiveUI、TypeScript、Vite 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Fastapi_NaiveVue) | +| Tornado+Layui混编版 | 基于 Tornado、Jinja2、Layui 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Tornado_Layui) | +| Tornado+ElementPlus前后端分离版 | 基于 Tornado、SQLAlchemy、Pydantic、Vue3、ElementPlus、TypeScript、Vite 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Tornado_EleVue) | +| Tornado+AntDesign前后端分离版 | 基于 Tornado、SQLAlchemy、Pydantic、Vue3、AntDesign、TypeScript、Vite 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Tornado_AntdVue) | +| Tornado+NaiveUI前后端分离版 | 基于 Tornado、SQLAlchemy、Pydantic、Vue3、NaiveUI、TypeScript、Vite 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Tornado_NaiveVue) | + + +## 模块展示 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +## 开源协议 + +开源软件遵循 [Apache 2.0 协议](https://www.apache.org/licenses/LICENSE-2.0.html)。 +允许商业使用,但务必保留包括但不限于开源团队、软件作者、版权说明等信息。 + +## 版权信息 + +版权所有 Copyright © 2021~2026 [djangoadmin.cn](https://www.djangoadmin.cn) All rights reserved。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/other/declare.md b/DjangoAdmin_Tornado_EleVue/zh/other/declare.md new file mode 100644 index 0000000..7737479 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/other/declare.md @@ -0,0 +1,71 @@ +# 免责声明 + +:::danger 重要提示 +任何用户在使用本团队研发的系列软件产品(以下简称「本产品」)前,请仔细阅读并透彻理解本声明。选择不使用本产品,可随时停止使用;一旦使用本产品,即视为对本声明全部内容的认可和接受。 +::: + +## 一、产品性质 + +1. 本产品是一套企业级数据中台解决方案的标准化软件架构,主要用于更高效地开发数据中台系统。本产品为标准化的后台管理框架,不涉及任何实际业务功能,不采集、存储或传输任何用户数据。 + +2. 本产品通过授权方式向企业和开发者提供使用许可。用户获得授权后,可在授权范围内使用和修改本产品,但不得以任何形式对外泄露、传播、转售或提供本产品源代码。 + +## 二、授权与使用规范 + +3. 用户应严格遵守本团队出具的《软件授权协议》,在授权范围内合理使用本产品。未经授权或超出授权范围的使用行为,本团队有权立即终止授权并追究法律责任。 + +4. 用户应采取必要的技术和管理措施,确保所授权产品的源代码安全,防止源代码泄露、被窃取或被未授权人员访问。因用户保管不善导致源代码泄露的,由用户承担全部责任。 + +5. 用户承诺秉持合法、合理的原则使用本产品,不利用本产品进行任何违法、侵害他人合法利益等恶意行为,亦不将本产品运用于任何违反中华人民共和国法律法规的 Web 平台。 + +6. 用户在使用本产品进行二次开发或集成第三方服务时,应自行评估并承担由此产生的技术风险、安全风险和法律风险。本团队不对用户二次开发后的系统稳定性、安全性和合规性承担责任。 + +7. 用户应自行负责生产环境的数据备份、安全防护和容灾方案。因用户自身操作不当、配置错误或未做好数据备份而导致的任何损失,本团队不承担责任。 + +## 三、知识产权 + +8. 本团队对本产品拥有完整的知识产权(包括但不限于商标权、专利权、著作权、商业秘密等),上述权利均受中华人民共和国及相关国际法律法规保护。 + +9. 任何单位或个人不得在未经本团队书面授权的情况下,对本产品本身申请任何形式的知识产权(包括但不限于软件著作权、专利、商标等)。 + +10. 任何单位或个人不得将本产品的源代码、文档、设计等知识产权用于开发与本产品功能实质性相似的竞品,或以任何形式损害本团队的商业利益。 + +11. 本产品包含的第三方源码和二进制文件之版权信息另行标注,用户使用时应遵守相应的第三方许可协议。 + +## 四、保密义务 + +12. 用户应对本产品的源代码、技术文档、架构设计、业务逻辑等一切技术信息承担保密义务。未经本团队书面同意,不得以任何形式向任何第三方披露、泄露或提供上述信息。 + +13. 用户不得将本产品源代码上传至任何公开代码仓库、代码分享平台或任何可被第三方访问的网络空间。 + +14. 用户不得对本产品进行反编译、反汇编或以其他方式试图获取本产品的源代码(授权范围内已提供源代码的情况除外)。 + +## 五、免责声明 + +15. 本产品按「现状」提供,不附带任何明示或暗示的担保,包括但不限于适销性、特定用途适用性、不侵权、无安全漏洞等担保。 + +16. 本团队不保证本产品无安全漏洞。如发现安全漏洞,本团队将尽力在合理时间内修复,但不对因安全漏洞导致的任何直接或间接损失承担责任。用户应自行做好安全防护措施。 + +17. 本团队不保证本产品服务的连续性和可用性。因系统维护、升级、不可抗力等原因导致的服务中断,本团队不承担责任。 + +18. 任何单位或个人因下载、安装、使用本产品而产生的任何意外、疏忽、合约毁坏、诽谤、版权或知识产权侵犯及其造成的损失(包括但不限于直接、间接、附带或衍生的损失),本团队不承担任何法律责任。 + +19. 用户明确并同意本声明条款列举的全部内容,对使用本产品可能存在的风险和相关后果将完全由用户自行承担,本团队不承担任何法律责任。 + +20. 本产品可能包含指向第三方网站或资源的链接。本团队对第三方网站或资源的可用性、内容、广告、产品或其他材料不承担任何责任。 + +## 六、违约责任 + +21. 用户违反本声明任一条款的,本团队有权立即终止授权,要求用户停止使用并销毁所有源代码副本,同时保留追究法律责任的权利。 + +22. 因用户违反本声明导致本团队遭受损失的(包括但不限于直接经济损失、商誉损失、维权合理费用等),用户应予以全额赔偿。 + +## 七、声明变更 + +23. 本团队有权随时对本声明条款及附件内容进行单方面的变更,并以消息推送、网页公告等方式予以公布,公布后立即自动生效,无需另行单独通知。若在本声明内容公告变更后继续使用的,表示已充分阅读、理解并接受修改后的声明内容。 + +24. 如果本声明的任何部分被认为无效或不可执行,则该部分将被解释为反映本团队的初衷,其余部分仍具有完全效力。不可执行的部分声明,并不构成放弃执行该声明的权利。 + +## 八、适用法律 + +25. 本声明的解释、效力及争议解决均适用中华人民共和国法律。因本声明产生的任何争议,双方应首先友好协商解决;协商不成的,任何一方均有权向本团队所在地有管辖权的人民法院提起诉讼。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/other/sponsor.md b/DjangoAdmin_Tornado_EleVue/zh/other/sponsor.md new file mode 100644 index 0000000..151e1ec --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/other/sponsor.md @@ -0,0 +1,40 @@ +# 广告赞助 + +:::tip 特别说明 +1. 投放广告不得与官方全系产品存在结构性冲突和竞争 +2. 广告内容需符合国家法律法规,不得包含虚假、误导性信息 +3. 广告素材需经官方审核通过后方可上线,审核周期 1-3 个工作日 +4. 广告位按签约顺序排期,先到先得,名额满后需等待下一期 +5. 合作期间如遇产品重大版本更新,官方有权提前 7 天通知调整展示位置 +6. 年费合作支持一次免费素材更换(限同尺寸),额外更换按次收费 +::: + +## 关于我们 + +小蚂蚁云团队专注于企业级后台管理解决方案,产品覆盖 Tornado、Django、Flask、Tornado 多技术栈,提供 ElementPlus / AntDesign / NaiveUI 多前端版本。官网月均访问量 10 万+,用户群体以企业开发者和技术决策者为主。 + +## 赞助优势 + +**精准触达**:用户群体以中大型企业开发者、技术负责人、架构师为主,与开发者工具、云服务、技术培训等产品高度契合。 + +**持续曝光**:品牌展示在官网首页、文档站侧边栏、文章页等显眼位置,获得长期稳定的流量曝光。 + +**灵活形式**:支持横幅广告、侧边栏广告、内容植入、专题合作等多种形式,满足不同推广需求。 + +**效果可追踪**:提供曝光量、点击率、转化率等数据报告,帮助实时优化投放策略。 + +## 合作方案 + +| 类别 | 展示位置 | 素材尺寸 | 名额 | 年费 | +|:-----|:---------|:---------|:-----|:-----| +| A1 | 全站头部 Banner | 2520×144px | 仅 1 位 | 50,000 元 | +| H1 | 首页青铜赞助商 | 1200×300px | 仅 4 位 | 24,000 元 | +| H2 | 首页白金赞助商 | 1200×300px | 仅 4 位 | 48,000 元 | +| D1 | 文档页侧边栏(小) | 1200×300px | 仅 4 位 | 48,000 元 | +| D2 | 文档页侧边栏(大) | 300×300px | 仅 3 位 | 36,000 元 | + +支持包年(推荐)、包月、按展示量(CPM)、按点击量(CPC)等灵活计费方式,亦可定制专属合作方案。 + +## 联系我们 + +若有投放意向,请发送邮件至 djangoadmin@126.com 咨询详细事宜。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/struct.md b/DjangoAdmin_Tornado_EleVue/zh/struct.md new file mode 100644 index 0000000..f5ecc54 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/struct.md @@ -0,0 +1,277 @@ +## 目录说明 + +在软件开发过程中,优秀的软件目录结构对于项目的组织、开发、维护和扩展至关重要,合理的目录结构能够显著提升开发效率和代码可维护性。 + +``` +1. 清晰的层次结构:按照功能模块划分目录,方便团队成员快速定位代码。 +2. 模块化管理:通过多模块结构,实现功能模块的独立管理,提高系统的可维护性和扩展性。 +3. 分层架构:Handler 层、Service 层、Repository 层、Model 层分离,职责清晰。 +4. 提升开发效率:清晰的目录结构和模块划分,减少查找代码的时间,提高开发效率。 +``` + +## 项目根目录 + +``` +├── src/ // 后端源码 +├── ui/ // 前端源码 +├── document/ // 项目文档 +├── migrations/ // 数据库迁移脚本 +├── scripts/ // 脚本工具 +├── tests/ // 测试代码 +├── wiki/ // 文档站点 +├── static/ // 静态资源 +├── uploads/ // 上传文件目录 +├── logs/ // 运行日志目录 +├── app.py // 启动入口 +├── .env.example // 环境变量模板 +├── .env // 环境变量 +├── docker-compose.yml // Docker 编排配置 +├── Dockerfile // 镜像构建文件 +├── Makefile // 快捷命令 +├── pyproject.toml // Poetry 依赖配置 +├── poetry.lock // Poetry 依赖锁定 +├── alembic.ini // Alembic 配置 +├── requirements.txt // Python 依赖清单 +└── README.md // 项目说明 +``` + +## 后端目录 (src/) + +``` +src/ +├── app.py // 应用工厂 +├── bootstrap.py // 启动引导 +│ +├── config/ // 配置模块 +│ ├── __init__.py +│ ├── app.py // 应用基础配置 +│ ├── auth.py // JWT 与登录安全配置 +│ ├── captcha.py // 验证码配置 +│ ├── database.py // 数据库配置 +│ ├── email.py // 邮件配置 +│ ├── logging.py // 日志配置 +│ └── redis.py // Redis 配置 +│ +├── core/ // 核心基础设施 +│ ├── __init__.py +│ ├── base_handler.py // 请求处理器基类 +│ ├── base_model.py // ORM 模型基类 +│ ├── base_repository.py // 通用 Repository 基类 +│ ├── base_service.py // 通用 Service 基类 +│ ├── base_schema.py // 通用 Schema 基类 +│ ├── context.py // 上下文变量 +│ ├── database.py // 数据库引擎与会话管理 +│ ├── exceptions.py // 异常体系 +│ ├── jwt.py // JWT 令牌处理 +│ ├── logger.py // 日志配置 +│ ├── metrics.py // 性能指标 +│ ├── password.py // 密码加密工具 +│ ├── redis.py // Redis 客户端 +│ ├── response.py // 统一响应封装 +│ └── tracer.py // 链路追踪 +│ +├── common/ // 公共模块 +│ ├── __init__.py +│ ├── permission.py // 权限解析 +│ ├── middleware/ // 中间件 +│ │ ├── __init__.py +│ │ ├── access_decorators.py // 访问控制装饰器 +│ │ ├── authentication.py // 认证中间件 +│ │ ├── cors.py // 跨域处理 +│ │ ├── login_log.py // 登录日志装饰器 +│ │ ├── metrics.py // 请求指标中间件 +│ │ ├── operation_log.py // 操作日志装饰器 +│ │ └── trace_id.py // 链路追踪中间件 +│ └── utils/ // 工具函数 +│ ├── __init__.py +│ ├── captcha.py // 验证码工具 +│ ├── crypto.py // 密码加密工具 +│ ├── dict_util.py // 数据字典工具 +│ ├── file.py // 文件工具 +│ ├── helpers.py // 通用工具函数 +│ ├── ip2region.py // IP 地理位置解析 +│ ├── rich_text.py // 富文本处理 +│ └── validators.py // 校验工具 +│ +├── modules/ // 业务模块 +│ ├── __init__.py +│ ├── auth/ // 认证模块 +│ ├── user/ // 用户管理 +│ ├── role/ // 角色管理 +│ ├── menu/ // 菜单管理 +│ ├── dept/ // 部门管理 +│ ├── position/ // 岗位管理 +│ ├── level/ // 职级管理 +│ ├── dict/ // 字典管理 +│ ├── dict_item/ // 字典项管理 +│ ├── config/ // 系统配置 +│ ├── config_item/ // 配置项管理 +│ ├── notice/ // 通知公告 +│ ├── link/ // 友情链接 +│ ├── category/ // 分类管理 +│ ├── article/ // 文章管理 +│ ├── param/ // 参数管理 +│ ├── city/ // 城市级联 +│ ├── file_template/ // 文件模板 +│ ├── job/ // 定时任务 +│ ├── job_log/ // 任务日志 +│ ├── upload/ // 文件上传 +│ ├── login_log/ // 登录日志 +│ ├── operation_log/ // 操作日志 +│ ├── user_role/ // 用户角色关联 +│ ├── role_menu/ // 角色菜单关联 +│ ├── example/ // 示例模块 +│ └── generator/ // 代码生成器 +│ +├── api/ // API 路由定义 +│ ├── __init__.py +│ └── v1/ // v1 版本路由 +│ ├── __init__.py +│ ├── router.py // 路由聚合 +│ ├── auth.py // 认证路由 +│ ├── index.py // 首页路由 +│ ├── upload.py // 上传路由 +│ ├── user.py // 用户路由 +│ ├── user_role.py // 用户角色路由 +│ ├── role.py // 角色路由 +│ ├── role_menu.py // 角色菜单路由 +│ ├── menu.py // 菜单路由 +│ ├── dept.py // 部门路由 +│ ├── dict.py // 字典路由 +│ ├── dict_item.py // 字典项路由 +│ ├── config.py // 配置路由 +│ ├── config_item.py // 配置项路由 +│ ├── level.py // 职级路由 +│ ├── position.py // 岗位路由 +│ ├── city.py // 城市路由 +│ ├── notice.py // 公告路由 +│ ├── link.py // 友链路由 +│ ├── category.py // 分类路由 +│ ├── article.py // 文章路由 +│ ├── param.py // 参数路由 +│ ├── file_template.py // 文件模板路由 +│ ├── job.py // 定时任务路由 +│ ├── job_log.py // 任务日志路由 +│ ├── generator.py // 代码生成器路由 +│ ├── operation_log.py // 操作日志路由 +│ ├── login_log.py // 登录日志路由 +│ └── example.py // 示例路由 +│ +├── extensions/ // 扩展注册 +│ ├── __init__.py // 扩展初始化 +│ ├── sqlalchemy.py // 数据库扩展 +│ ├── redis.py // Redis 扩展 +│ ├── swagger.py // Swagger 占位 +│ └── sentry.py // Sentry 占位 +│ +└── constants/ // 常量定义 + └── __init__.py // 常量定义 +``` + +## 前端目录 (ui/src/) + +``` +ui/src/ +├── main.ts // 入口文件 +├── App.vue // 根组件 +├── api/ // API 接口定义 +│ ├── system/ // 系统管理接口 +│ ├── content/ // 内容管理接口 +│ ├── data/ // 数据管理接口 +│ ├── monitor/ // 监控管理接口 +│ ├── tool/ // 工具接口 +│ ├── file/ // 文件接口 +│ ├── region/ // 地区接口 +│ ├── setting/ // 设置接口 +│ ├── dashboard/ // 仪表盘接口 +│ └── common/ // 公共接口 +├── views/ // 页面视图 +│ ├── login/ // 登录页 +│ ├── dashboard/ // 控制台 +│ ├── system/ // 系统管理页面 +│ ├── content/ // 内容管理页面 +│ ├── data/ // 数据管理页面 +│ ├── monitor/ // 监控管理页面 +│ ├── file/ // 文件管理页面 +│ ├── tool/ // 工具页面 +│ ├── setting/ // 设置页面 +│ ├── exception/ // 异常页面 +│ └── about/ // 关于页面 +├── components/ // 公共组件 +│ ├── Table/ // 表格组件 +│ ├── Form/ // 表单组件 +│ ├── Modal/ // 弹窗组件 +│ ├── Page/ // 页面容器 +│ ├── Upload/ // 上传组件 +│ ├── Editor/ // 富文本编辑器 +│ ├── Cropper/ // 图片裁剪组件 +│ ├── Qrcode/ // 二维码组件 +│ ├── Excel/ // Excel 导入导出组件 +│ ├── ChinaArea/ // 省市区联动组件 +│ ├── TableSelect/ // 表格选择器组件 +│ ├── Select/ // 下拉选择组件 +│ ├── CountTo/ // 数字动画组件 +│ ├── Authority/ // 权限组件 +│ ├── Lockscreen/ // 锁屏组件 +│ ├── Password/ // 密码强度组件 +│ ├── Region/ // 区域选择组件 +│ ├── numberInput/ // 数字输入组件 +│ ├── priceInput/ // 价格输入组件 +│ ├── pagination/ // 分页组件 +│ ├── Render/ // 渲染组件 +│ ├── icon/ // 图标组件 +│ └── importFile/ // 文件导入组件 +├── hooks/ // 组合式函数 +│ ├── core/ // 核心 hooks +│ ├── event/ // 事件 hooks +│ ├── setting/ // 配置 hooks +│ ├── web/ // Web hooks +│ ├── use-async.ts // 异步请求状态管理 +│ ├── useBattery.ts // 电量监听 +│ ├── useDomWidth.ts // DOM 宽度监听 +│ ├── useOnline.ts // 网络状态监听 +│ └── useTime.ts // 时间工具 +├── store/ // 状态管理 +│ ├── index.ts // Store 入口 +│ ├── modules/ // Store 模块 +│ ├── plugins/ // Store 插件 +│ └── types.ts // 类型定义 +├── router/ // 路由配置 +│ ├── index.ts // 路由实例 +│ ├── base.ts // 静态路由 +│ ├── constant.ts // 常量路由 +│ ├── generator-routers.ts // 动态路由生成 +│ ├── router-guards.ts // 路由守卫 +│ ├── router-icons.ts // 路由图标映射 +│ ├── menus/ // 菜单配置 +│ └── types.ts // 类型定义 +├── utils/ // 工具函数 +│ ├── http/ // HTTP 请求封装 +│ ├── auth.ts // Token 管理 +│ ├── dateUtil.ts // 日期格式化 +│ ├── downloadFile.ts // 文件下载 +│ ├── validate.ts // 表单校验 +│ ├── env.ts // 环境变量 +│ ├── color.ts // 颜色工具 +│ ├── wartermark.ts // 水印 +│ ├── useLockFn.ts // 防重复提交 +│ └── Storage.ts // 本地存储 +├── directives/ // 自定义指令 +├── plugins/ // 插件注册 +├── styles/ // 全局样式 +├── enums/ // 枚举定义 +└── assets/ // 静态资源 + ├── icons/ // 图标 + └── images/ // 图片 +``` + +:::tip 注意事项 ++ 保持一致性:目录结构一旦确定,应保持一致性,避免随意更改。 ++ 合理划分模块:根据项目的实际需求,合理划分功能模块,避免过度模块化。 ++ 文档记录:在项目文档中详细说明目录结构和各模块的功能,便于新成员快速上手。 ++ 版本控制:使用 `Git` 等版本控制工具,并在 `.gitignore` 文件中配置不需要跟踪的目录和文件。 +::: + +## 总结 + +软件架构的目录结构是项目开发和维护的基础,直接影响到项目的可维护性、可扩展性和开发效率。采用分层的目录结构(后端 handler → service → repository → model,前端 api → views → components),结合模块化管理,是一个有效的解决方案。通过合理规划和遵循最佳实践,可以确保项目的结构清晰、功能明确,为团队开发和长期维护打下坚实的基础。 \ No newline at end of file diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume1/backend.md b/DjangoAdmin_Tornado_EleVue/zh/volume1/backend.md new file mode 100644 index 0000000..9835442 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume1/backend.md @@ -0,0 +1,339 @@ +# 后端启动 + +本章节介绍如何将后端项目从官网下载到本地并成功启动运行。后端基于 `Python` + `Tornado` 框架,采用分层架构设计(Handler 层 → Service 层 → Repository 层 → Model 层)。后端要求 Python 3.12+ 版本。 + +:::tip 温馨提示 +启动后端前,请确保已完成 [环境准备](/zh/volume1/environment) 章节中的所有软件安装。 +::: + +## 获取源码 + +前往 [官方网站](https://www.djangoadmin.cn) 购买授权后,按以下步骤获取源码: + +``` +1. 登录官方网站,进入「个人中心」→「我的订单」页面。 +2. 在订单列表中找到已购买的授权订单,点击「下载」按钮。 +3. 下载的压缩包包含完整的前后端源码、数据库脚本及部署配置文件。 +4. 将压缩包解压到本地开发目录(如 E:\Projects\ 或 ~/Projects/)。 +``` + +```bash +# 进入项目根目录(以实际解压路径为准) +cd DjangoAdmin_Tornado_EleVue +``` + +:::tip 温馨提示 +1. 源码包请务必从官方网站订单中心下载,确保获取的是正版授权的最新版本。 +2. 授权有效期内可无限次下载最新版本,版本更新后可重新下载获取最新源码。 +3. 解压后请先阅读根目录下的 `README.md` 和 `CHANGELOG.md` 了解版本更新内容。 +::: + +项目目录结构如下: + +``` +├── app.py # 启动入口(__main__) +├── src/ # 后端源码 +│ ├── app.py # 应用工厂(create_app) +│ ├── bootstrap.py # 启动引导(信号处理、优雅关闭) +│ ├── config/ # 配置模块 +│ ├── core/ # 核心基类 +│ ├── common/ # 公共模块 +│ ├── modules/ # 业务模块 +│ ├── api/v1/ # 路由定义 +│ └── extensions/ # 扩展注册(sqlalchemy/redis) +├── ui/ # 前端源码(Vue3 + Element Plus) +├── scripts/ # 脚本工具(数据库初始化、数据迁移等) +├── document/ # 数据库脚本 +├── tests/ # 测试代码 +├── .env.example # 环境变量示例文件 +├── pyproject.toml # Poetry 依赖配置 +├── poetry.lock # Poetry 依赖锁定 +├── Dockerfile # Docker 镜像构建 +├── docker-compose.yml # Docker 编排文件 +└── Makefile # 常用命令集合 +``` + +## 安装依赖 + +进入项目根目录,使用 `Poetry` 安装后端依赖包: + +```bash +# 安装 Poetry(如果尚未安装) +pip install poetry + +# 安装项目依赖 +poetry install +``` + +:::tip 温馨提示 +如果网络较慢,可先配置 pip 镜像源(参见 [环境准备](/zh/volume1/environment)),或使用 Poetry 镜像: + +```bash +poetry config repositories.mirror https://mirrors.aliyun.com/pypi/simple/ +``` +::: + ++ 验证依赖安装 + +```bash +# 检查 Tornado 是否安装成功 +poetry run python -c "import tornado; print(tornado.version)" +6.4.2 + +# 检查 SQLAlchemy 是否安装成功 +poetry run python -c "import sqlalchemy; print(sqlalchemy.__version__)" +2.0.51 +``` + +:::tip 温馨提示 +依赖安装完成后,如果上述命令输出版本号,说明核心依赖已正确安装。 +::: + +## 配置环境变量 + +项目通过 `.env` 文件管理所有配置参数。首次使用需要从示例文件复制并修改: + +```bash +# 复制环境变量示例文件 +cp .env.example .env # Linux / macOS +copy .env.example .env # Windows CMD +``` + ++ 编辑 `.env` 文件 + +使用文本编辑器打开 `.env` 文件,修改以下关键配置: + +```bash +# ============================================================ +# 基础配置 +# ============================================================ +TORNADO_NAME=Tornado+EleVue旗舰版 +TORNADO_VERSION=v3.0.0 +TORNADO_HOST=127.0.0.1 +TORNADO_PORT=8041 +TORNADO_ENV=development +TORNADO_DEBUG=True + +# ============================================================ +# 数据库配置(根据实际情况修改) +# ============================================================ +DB_DRIVER=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=djangoadmin.tornado.elevue +DB_USERNAME=root +DB_PASSWORD=your_mysql_password # 修改为真实密码 +DB_PREFIX=tornado_ + +# ============================================================ +# Redis 缓存配置(根据实际情况修改) +# ============================================================ +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 +REDIS_PASSWORD=your_redis_password # 修改为真实密码 +REDIS_AUTH=True + +# ============================================================ +# JWT 令牌配置(必填项) +# ============================================================ +# 生成方式:python -c "import secrets; print(secrets.token_urlsafe(48))" +JWT_SECRET=your_jwt_secret_key_at_least_32_bytes +JWT_EXPIRE_MINUTES=20 +``` + +:::danger 重要提示 +1. `JWT_SECRET` 是 JWT 令牌的签名密钥,**必须设置**且至少 32 字节。留空或过弱会导致应用拒绝启动(fail-close)。可使用以下命令生成: + +```bash +python -c "import secrets; print(secrets.token_urlsafe(48))" +``` + +2. `DB_PASSWORD` 和 `REDIS_PASSWORD` 请填写真实的服务密码。 +3. 生产环境务必设置 `TORNADO_DEBUG=False`,避免泄漏内部错误信息。 +::: + +## 初始化数据库 + +首次启动前,需要初始化数据库结构和种子数据。 + +### 方式一:手动导入(推荐) + +```bash +# 1. 登录 MySQL,创建数据库 +mysql -uroot -p +CREATE DATABASE `djangoadmin.tornado.elevue` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + +# 2. 导入数据库脚本 +mysql -uroot -p djangoadmin.tornado.elevue < document/djangoadmin.tornado.elevue.sql +``` + +### 方式二:Alembic 迁移(备选) + +```bash +# 自动建表 +poetry run alembic upgrade head + +# 导入业务字典种子数据 +python scripts/seed_dict.py +``` + +:::tip 温馨提示 +项目支持 MySQL、PostgreSQL、SQL Server、SQLite、Oracle 等数据库。通过 `.env` 中的 `DB_DRIVER` 切换驱动(默认 `mysql`)。非 MySQL 数据库请使用 Alembic + seed 脚本初始化。详细的多数据库初始化步骤请参考 [数据库初始化](/zh/volume1/database) 章节。 +::: + +初始化完成后,数据库中包含以下默认数据: + +``` +管理员账号:admin / 123456(超级管理员,ID=1,跳过权限校验) +默认角色:超级管理员角色 +默认菜单:系统管理、内容管理、监控管理等菜单及权限节点 +数据字典:系统内置字典数据 +系统配置:系统默认配置项 +``` + +## 启动后端服务 + +一切准备就绪后,执行以下命令启动后端服务: + +```bash +# 使用 Poetry 启动(推荐) +poetry run python app.py --port=8041 + +# 或直接 python 启动 +python app.py --port=8041 +``` + +:::tip 启动入口说明 +启动流程:`app.py`(__main__) → `src/bootstrap.py:main()` → `src/app.py:create_app()` + +`bootstrap.py` 负责解析命令行参数(`--port`)、创建应用、注册 SIGINT/SIGTERM 信号实现优雅关闭。 + +`create_app()` 是应用工厂函数(位于 `src/app.py`),负责组装路由、配置模板/静态目录、初始化扩展。 +::: + +启动成功后,终端将输出类似以下信息: + +``` + ____ _ _ + | _ \ _ _ __ _ _ __ ___ (_) _ __ __ _ _ __ __| | + | | | || | | | / _` || '__| / __|| || '_ \ / _` || '__| / _` | + | |_| || |_| || (_| || | | (__ | || |_) || (_| || | | (_| | + |____/ \__,_| \__,_||_| \___||_|| .__/ \__,_||_| \__,_| + |_| + Tornado + EleVue 企业级管理框架 + +[INFO] 服务启动成功 → http://127.0.0.1:8041 +``` + +:::tip 默认端口 +后端服务默认监听端口为 `8041`,可通过 `--port` 参数或 `.env` 文件中的 `TORNADO_PORT` 参数修改。 +::: + +## 验证后端服务 + ++ 测试接口连通性 + +使用 `curl` 或浏览器访问健康检查接口: + +```bash +# 测试服务是否正常响应 +curl http://127.0.0.1:8041/api/v1/health +``` + +返回以下 JSON 即表示服务正常运行: + +```json +{ + "code": 0, + "data": { + "status": "ok" + }, + "msg": "操作成功", + "ok": true +} +``` + ++ 测试验证码接口 + +```bash +curl http://127.0.0.1:8041/api/v1/captcha +``` + +返回类似以下 JSON: + +```json +{ + "code": 0, + "data": { + "captcha": "data:image/png;base64,...", + "key": "sl1dhi-ejqV2PWH8tYDE1pVNaRiccsrQ" + }, + "msg": "操作成功", + "ok": true +} +``` + ++ 查看日志输出 + +在终端中观察后端日志输出,确认无异常错误信息。如果出现连接数据库或 `Redis` 失败的错误,请检查 `.env` 配置和相关服务是否正常运行。 + +## Makefile 常用命令 + +项目根目录的 `Makefile` 提供了常用命令的快捷方式。 + +:::tip Windows 用户注意 +`make` 命令是 Linux / macOS 系统自带的构建工具,Windows 系统默认不包含。Windows 用户可直接执行表格中"对应命令"列所示的原始命令。 +::: + +| 命令 | 对应命令 | 说明 | +|:----|:----|:----| +| `make install` | `poetry install` | 安装依赖 | +| `make dev` | `poetry run python app.py --port=8041` | 启动开发服务 | +| `make test` | `poetry run pytest` | 运行全部测试 | +| `make test-unit` | `poetry run pytest tests/unit` | 运行单元测试 | +| `make lint` | `poetry run ruff check src tests` | 代码规范检查 | +| `make seed-dict` | `python scripts/seed_dict.py` | 初始化业务字典数据 | + +## 常见问题 + ++ 端口被占用 + +如果启动时报错 `Address already in use`,说明端口 `8041` 已被其他程序占用: + +```bash +# Windows 查看端口占用 +netstat -ano | findstr 8041 + +# Linux / macOS 查看端口占用 +lsof -i :8041 +``` + +解决方式:终止占用进程,或使用 `--port` 参数指定其他端口。 + ++ JWT_SECRET 未配置 + +如果启动时报错 `JWT_SECRET` 相关错误,说明未配置 JWT 签名密钥或密钥过弱。请在 `.env` 文件中设置: + +```bash +JWT_SECRET=your_generated_secret_key_here_at_least_32_bytes +``` + +生成方式: + +```bash +python -c "import secrets; print(secrets.token_urlsafe(48))" +``` + ++ 数据库连接失败 + +如果启动时报错数据库连接失败,请检查: + +``` +1. MySQL 服务是否已启动 +2. .env 中的 DB_HOST、DB_PORT、DB_USERNAME、DB_PASSWORD 是否正确 +3. 防火墙是否放行了数据库端口 +``` + +## 总结 + +本章节介绍了后端项目的完整启动流程:拉取代码 → 安装依赖 → 配置环境变量 → 初始化数据库 → 启动服务 → 验证接口。通过以上步骤,你已经成功将后端服务运行在本地 `8041` 端口。下一步可以进入 [前端启动](/zh/volume1/frontend) 章节,启动前端项目并登录系统。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume1/database.md b/DjangoAdmin_Tornado_EleVue/zh/volume1/database.md new file mode 100644 index 0000000..9468a4f --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume1/database.md @@ -0,0 +1,197 @@ +# 数据库初始化 + +本章节介绍如何初始化项目数据库,包括建库、建表、导入种子数据以及后续的数据库迁移管理。 + +项目支持 MySQL、PostgreSQL、SQL Server、SQLite、Oracle 等多种数据库,通过 `.env` 中的 `DB_DRIVER` 切换驱动。 + +:::tip 温馨提示 +数据库初始化前,请确保已完成以下准备工作: +1. 目标数据库服务已启动(如 MySQL 8.0+) +2. `.env` 文件中的 `DB_DRIVER` 和数据库连接信息已正确配置 +3. Python 依赖已安装(`poetry install`) +::: + +## 方式一:手动初始化(推荐) + +手动初始化是最直接、最可控的方式,适合开发环境和生产环境。 + +### 第一步:创建数据库 + +根据你使用的数据库类型,手动创建数据库。 + +**MySQL**: + +```sql +CREATE DATABASE `djangoadmin.tornado.elevue` + DEFAULT CHARACTER SET utf8mb4 + COLLATE utf8mb4_general_ci; +``` + +**PostgreSQL**: + +```sql +CREATE DATABASE "djangoadmin.tornado.elevue" + WITH ENCODING 'UTF8' + LC_COLLATE = 'zh_CN.UTF-8' + LC_CTYPE = 'zh_CN.UTF-8'; +``` + +**SQL Server**: + +```sql +CREATE DATABASE [djangoadmin.tornado.elevue]; +``` + +### 第二步:导入数据库脚本 + +MySQL 数据库脚本位于 `document/` 目录下: + +```bash +# 登录 MySQL +mysql -uroot -p + +# 导入脚本 +mysql -uroot -p djangoadmin.tornado.elevue < document/djangoadmin.tornado.elevue.sql +``` + +:::tip 温馨提示 +`document/djangoadmin.tornado.elevue.sql` 是 MySQL 专用脚本。对于 PostgreSQL / SQL Server / SQLite / Oracle,建议使用 Alembic 方式初始化。 +::: + +### 第三步:导入字典种子数据 + +```bash +# 导入业务字典数据(性别、友链类型、通知来源等) +python scripts/seed_dict.py +``` + +:::tip 温馨提示 +`seed_dict.py` 是幂等脚本,可重复执行不会产生重复数据。 +::: + +## 方式二:Alembic 迁移初始化 + +适合非 MySQL 数据库,或需要版本化管理数据库结构的场景。 + +### 第一步:创建数据库 + +同方式一的手动建库步骤。 + +### 第二步:执行迁移 + +```bash +# 根据 models 定义自动建表 +poetry run alembic upgrade head +``` + +### 第三步:导入种子数据 + +```bash +python scripts/seed_dict.py +``` + +## 方式三:脚本初始化(备选) + +```bash +# 自动完成建库、建表、导入种子数据 +python scripts/init_db.py +``` + +:::tip 温馨提示 +`init_db.py` 会根据 `.env` 中的 `DB_DRIVER` 配置自动初始化数据库。适合快速搭建开发环境。 +::: + +## 多数据库驱动配置 + +项目通过 `DB_DRIVER` 环境变量切换数据库驱动: + +| DB_DRIVER | 别名 | Python 驱动 | 安装方式 | +|:----------|:-----|:------------|:---------| +| `mysql` | — | pymysql | 默认已安装 | +| `postgresql` | `postgres` | psycopg2-binary | `poetry install -E postgresql` | +| `mssql` | `sqlserver` | pymssql | `poetry install -E mssql` | +| `sqlite` | `sqlite3` | sqlite3 | 内置,无需额外安装 | +| `oracle` | — | oracledb | `poetry install -E oracle` | + +### SQLite 配置示例 + +```bash +# .env +DB_DRIVER=sqlite +DB_DATABASE=/path/to/djangoadmin.db +``` + +:::tip SQLite 说明 +当 `DB_DRIVER=sqlite` 时,`DB_DATABASE` 为数据库文件的完整路径(相对或绝对路径),`DB_HOST`、`DB_PORT`、`DB_USERNAME`、`DB_PASSWORD` 均被忽略。 +::: + +### PostgreSQL 配置示例 + +```bash +# .env +DB_DRIVER=postgresql +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_DATABASE=djangoadmin.tornado.elevue +DB_USERNAME=postgres +DB_PASSWORD=your_password +``` + +### Oracle 配置示例 + +```bash +# .env +DB_DRIVER=oracle +DB_HOST=127.0.0.1 +DB_PORT=1521 +DB_DATABASE=XEPDB1 # SERVICE_NAME +DB_USERNAME=system +DB_PASSWORD=your_password +``` + +## 初始化完成后 + +数据库初始化完成后,系统包含以下默认数据: + +| 数据 | 说明 | +|:-----|:-----| +| 管理员账号 | admin / 123456(超级管理员,ID=1,跳过权限校验) | +| 默认角色 | 超级管理员角色 | +| 默认菜单 | 系统管理、内容管理、监控管理等菜单及权限节点 | +| 数据字典 | 性别、状态、类型等系统内置字典数据 | +| 系统配置 | 系统默认配置项 | + +## 跨库数据迁移 + +项目提供 `scripts/migrate_db.py` 脚本,支持在不同数据库之间迁移数据(如 MySQL → PostgreSQL)。详细说明请参考 [跨库数据迁移](/zh/volume6/db-cross-migrate) 章节。 + +## 常见问题 + ++ 数据库连接失败 + +``` +1. 检查数据库服务是否已启动 +2. 检查 .env 中的 DB_HOST、DB_PORT、DB_USERNAME、DB_PASSWORD 是否正确 +3. 检查防火墙是否放行了数据库端口 +4. 检查数据库用户是否有建库/建表权限 +``` + ++ 导入脚本报错 + +``` +1. 确认数据库字符集为 utf8mb4(MySQL) +2. 确认数据库版本符合要求(MySQL 8.0+) +3. 检查脚本文件是否完整(未被截断) +``` + ++ Alembic 迁移失败 + +``` +1. 确认 alembic.ini 中的数据库连接配置正确 +2. 检查是否存在版本冲突:poetry run alembic history +3. 如需重置:poetry run alembic downgrade base,然后 poetry run alembic upgrade head +``` + +## 总结 + +本章节介绍了三种数据库初始化方式:手动导入 SQL 脚本(推荐)、Alembic 迁移、脚本自动初始化。项目支持 MySQL / PostgreSQL / SQL Server / SQLite / Oracle 五种数据库,通过 `DB_DRIVER` 环境变量切换。初始化完成后即可启动后端服务。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume1/docker.md b/DjangoAdmin_Tornado_EleVue/zh/volume1/docker.md new file mode 100644 index 0000000..16e4811 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume1/docker.md @@ -0,0 +1,516 @@ +# Docker 快速部署 + +本章节介绍如何使用 Docker 快速部署后端服务,无需手动安装 Python、MySQL、Redis 等环境。 + +:::tip 温馨提示 +Docker 部署适合快速体验和生产环境。本地开发建议使用 [后端启动](/zh/volume1/backend) 章节的方式。 +::: + +## 前置条件 + +确保已安装 Docker 和 Docker Compose: + +```bash +# 验证 Docker 安装 +docker --version +# Docker version 24.0.x + +# 验证 Docker Compose 安装 +docker compose version +# Docker Compose version v2.x.x +``` + +## 快速启动 + +### 1. 准备环境变量 + +```bash +# 复制环境变量模板 +cp .env.example .env + +# 编辑 .env,配置数据库密码、Redis 密码、JWT 密钥等 +``` + +:::danger 重要提示 +Docker 部署时,`.env` 中的 `DB_HOST` 和 `REDIS_HOST` 应指向 Docker 网络中的服务名称(如 `mysql`、`redis`),而非 `127.0.0.1`。 +::: + +### 2. 使用 Docker Compose 启动 + +```bash +# 构建并启动所有服务 +docker compose up -d --build + +# 查看服务状态 +docker compose ps + +# 查看日志 +docker compose logs -f app +``` + +### 3. 访问服务 + +``` +后端 API: http://127.0.0.1:8041/api/v1/health +``` + +## Dockerfile 说明 + +项目使用多阶段构建,减小镜像体积: + +```dockerfile +# ====================================================================== +# Stage 1: 构建依赖 +# 使用一个临时镜像来解析 Poetry 依赖,避免把 Poetry 本身 +# 和构建缓存带入最终运行镜像,从而减小镜像体积。 +# ====================================================================== +FROM python:3.12-slim AS builder + +# 设置构建阶段的工作目录 +WORKDIR /build + +# 只拷贝依赖描述文件(先拷这些可以利用 Docker 层缓存: +# 只要依赖没变,后续 poetry export 就不会重新执行) +COPY pyproject.toml poetry.lock* ./ + +# 安装 Poetry,并把 pyproject.toml / poetry.lock 中的依赖 +# 导出为普通的 requirements.txt,方便运行阶段用 pip 安装。 +# --without-hashes:不写入哈希校验,简化后续安装。 +RUN pip install --no-cache-dir poetry \ + && poetry export -f requirements.txt -o requirements.txt --without-hashes + + +# ====================================================================== +# Stage 2: 运行 +# 基于干净的 python:3.12-slim 构建最终镜像,只保留运行所需内容。 +# ====================================================================== +FROM python:3.12-slim AS runtime + +# 环境变量配置: +# PYTHONUNBUFFERED=1 → 不缓冲 stdout/stderr,日志实时输出 +# PYTHONDONTWRITEBYTECODE=1 → 不生成 .pyc 文件,保持镜像干净 +# TZ=Asia/Shanghai → 设置容器时区为上海 +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + TZ=Asia/Shanghai + +# 设置应用运行的工作目录 +WORKDIR /data/apps + +# 从构建阶段拷贝导出的 requirements.txt +COPY --from=builder /build/requirements.txt ./ + +# 安装运行所需的 Python 依赖 +RUN pip install --no-cache-dir -r requirements.txt + +# 拷贝应用代码和资源文件到镜像中 +COPY src ./src # 源码目录 +COPY app.py ./ # 应用入口 +COPY alembic ./alembic # 数据库迁移脚本 +COPY alembic.ini ./ # 数据库迁移配置 +COPY .env.example ./.env # 环境变量示例(默认作为 .env 使用) +COPY scripts ./scripts # 辅助脚本 +COPY templates ./templates # 模板文件 +COPY static ./static # 静态资源 + +# 创建运行时需要的上传目录和日志目录 +RUN mkdir -p uploads logs + +# 声明容器对外暴露的端口 +EXPOSE 8041 + +# 容器启动命令:运行 Tornado 应用入口 +CMD ["python", "app.py"] +``` + +### 多阶段构建原理 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Stage 1: builder │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ python:3.12-slim │ │ +│ │ + Poetry │ │ +│ │ + pyproject.toml / poetry.lock │ │ +│ │ ─────────────────────────────────────────────────────── │ │ +│ │ → poetry export → requirements.txt │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ 仅拷贝 requirements.txt │ +│ Stage 2: runtime │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ python:3.12-slim │ │ +│ │ + pip install -r requirements.txt │ │ +│ │ + 应用源码 + 配置 + 脚本 │ │ +│ │ ─────────────────────────────────────────────────────── │ │ +│ │ → 最终镜像(不含 Poetry、不含构建缓存) │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## docker-compose.yml 说明 + +### 基础版(仅应用服务) + +项目自带的 `docker-compose.yml` 仅包含应用服务,适用于数据库和 Redis 已有外部实例的场景: + +```yaml +# ====================================================================== +# Docker Compose 编排文件(基础版) +# 仅包含应用服务,适用于数据库和 Redis 已有外部实例的场景 +# 使用方式:docker compose up -d +# 依赖根目录下的 .env 提供环境变量 +# ====================================================================== + +services: + # ==================================================================== + # 应用服务:构建并运行本项目 + # ==================================================================== + tornado_elevue: + image: tornado_elevue:latest # 镜像名称 + container_name: tornado_elevue # 容器名称 + build: + context: ./ # 构建上下文为项目根目录 + dockerfile: Dockerfile # 使用项目根目录的 Dockerfile + restart: always # 总是自动重启(生产环境推荐) + ports: + - "8041:8041" # 宿主机端口:容器端口 + environment: + - TORNADO_PORT=8041 # 应用监听端口 + - TORNADO_ENV=production # 运行环境设为生产模式 + env_file: + - .env # 从 .env 文件加载数据库密码等环境变量 + volumes: + - ./uploads:/data/apps/uploads # 持久化上传文件到宿主机 + - ./logs:/data/apps/logs # 持久化日志(与 Dockerfile WORKDIR 对应) + command: python app.py # 启动命令 + logging: + driver: "json-file" # 日志驱动:JSON 文件 + options: + max-size: "500m" # 单个日志文件最大 500MB + max-file: "10" # 最多保留 10 个日志文件 +``` + +### 完整版(应用 + MySQL + Redis) + +快速体验或小型部署推荐使用包含 MySQL 和 Redis 的完整版: + +```yaml +# ====================================================================== +# Docker Compose 编排文件(完整版) +# 包含三个服务:app(应用)、mysql(数据库)、redis(缓存) +# 使用方式:docker compose up -d +# 依赖根目录下的 .env 提供密码等环境变量 +# ====================================================================== + +services: + # ==================================================================== + # 应用服务:构建并运行本项目 + # ==================================================================== + app: + build: . # 使用当前目录的 Dockerfile 构建镜像 + ports: + - "8041:8041" # 宿主机端口:容器端口(供外部访问) + env_file: + - .env # 从 .env 文件加载环境变量(数据库密码等) + environment: + - TORNADO_ENV=production # 运行环境设为生产模式 + - DB_HOST=mysql # 数据库地址指向 Docker 网络中的 mysql 服务 + - REDIS_HOST=redis # Redis 地址指向 Docker 网络中的 redis 服务 + volumes: + - ./uploads:/data/apps/uploads # 持久化上传文件到宿主机 + - ./logs:/data/apps/logs # 持久化日志(与 Dockerfile WORKDIR 对应) + depends_on: + - mysql # 声明依赖,但不保证 mysql 已就绪 + - redis + restart: unless-stopped # 除非手动停止,否则总是自动重启 + + # ==================================================================== + # MySQL 数据库 + # ==================================================================== + mysql: + image: mysql:8.0 # 官方 MySQL 8.0 镜像 + environment: + MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} # root 密码,从 .env 读取 + MYSQL_DATABASE: ${DB_DATABASE} # 初始化时自动创建的数据库名 + ports: + - "3306:3306" # 暴露到宿主机(生产环境建议去掉) + volumes: + - mysql_data:/var/lib/mysql # 命名卷持久化数据库数据 + restart: unless-stopped + + # ==================================================================== + # Redis 缓存 + # ==================================================================== + redis: + image: redis:7-alpine # 轻量级 Redis 7 镜像 + command: redis-server --requirepass ${REDIS_PASSWORD} # 启动时设置访问密码 + ports: + - "6379:6379" # 暴露到宿主机(生产环境建议去掉) + volumes: + - redis_data:/data # 持久化 Redis 数据 + restart: unless-stopped + +# ====================================================================== +# 命名卷:由 Docker 管理,数据存放在宿主机 /var/lib/docker/volumes 下 +# ====================================================================== +volumes: + mysql_data: # MySQL 数据持久化卷 + redis_data: # Redis 数据持久化卷 +``` + +### 完整版 .env 配置示例 + +使用完整版 docker-compose.yml 时,`.env` 文件中的数据库和 Redis 地址应指向 Docker 服务名称: + +```bash +# ============================================================ +# 数据库配置(指向 Docker 网络中的 mysql 服务) +# ============================================================ +DB_DRIVER=mysql +DB_HOST=mysql # Docker 服务名称,不是 127.0.0.1 +DB_PORT=3306 +DB_DATABASE=djangoadmin.tornado.elevue +DB_USERNAME=root +DB_PASSWORD=your_mysql_password # 与 MYSQL_ROOT_PASSWORD 一致 +DB_PREFIX=tornado_ + +# ============================================================ +# Redis 配置(指向 Docker 网络中的 redis 服务) +# ============================================================ +REDIS_HOST=redis # Docker 服务名称,不是 127.0.0.1 +REDIS_PORT=6379 +REDIS_PASSWORD=your_redis_password +REDIS_AUTH=True + +# ============================================================ +# JWT 配置(必填) +# ============================================================ +JWT_SECRET=your_jwt_secret_key_at_least_32_bytes + +# ============================================================ +# 应用配置 +# ============================================================ +TORNADO_ENV=production +TORNADO_DEBUG=False +``` + +## 常用 Docker 命令 + +```bash +# 构建镜像 +docker compose build + +# 启动服务(后台运行) +docker compose up -d + +# 停止服务 +docker compose down + +# 停止服务并删除数据卷(⚠️ 会丢失数据) +docker compose down -v + +# 查看日志(实时跟踪) +docker compose logs -f app + +# 查看所有服务日志 +docker compose logs -f + +# 进入容器 +docker compose exec app bash + +# 重启服务 +docker compose restart app + +# 查看容器状态 +docker compose ps + +# 查看容器资源占用 +docker stats +``` + +## 数据持久化 + +Docker Compose 使用命名卷和绑定挂载持久化数据: + +| 存储方式 | 卷名/路径 | 说明 | +|:---------|:----------|:-----| +| 命名卷 | `mysql_data` | MySQL 数据文件(由 Docker 管理) | +| 命名卷 | `redis_data` | Redis 数据文件(由 Docker 管理) | +| 绑定挂载 | `./uploads` | 上传文件(映射到宿主机) | +| 绑定挂载 | `./logs` | 日志文件(映射到宿主机) | + +:::warning 数据备份 +生产环境定期备份数据库: + +```bash +# 备份 MySQL +docker compose exec mysql mysqldump -uroot -p${DB_PASSWORD} ${DB_DATABASE} > backup_$(date +%Y%m%d).sql + +# 恢复 MySQL +docker compose exec -T mysql mysql -uroot -p${DB_PASSWORD} ${DB_DATABASE} < backup.sql +``` +::: + +## 生产环境部署建议 + +### 1. 关闭调试模式 + +```bash +# .env +TORNADO_DEBUG=False +TORNADO_ENV=production +``` + +### 2. 使用强密钥 + +```bash +# 生成 JWT 密钥 +python -c "import secrets; print(secrets.token_urlsafe(48))" +``` + +### 3. 配置 Nginx 反向代理 + +```nginx +server { + # 监听 80 端口(HTTP) + listen 80; + + # 域名配置(替换为实际域名) + server_name your-domain.com; + + # 反向代理配置:将所有请求转发到 Tornado 应用 + location / { + # 转发目标:Tornado 应用监听的地址和端口 + proxy_pass http://127.0.0.1:8041; + + # 传递原始请求头,让后端获取真实的客户端信息 + proxy_set_header Host $host; # 原始域名 + proxy_set_header X-Real-IP $remote_addr; # 客户端真实 IP + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 代理链 IP 列表 + proxy_set_header X-Forwarded-Proto $scheme; # 原始协议(http/https) + + # 超时配置(根据业务需求调整) + proxy_connect_timeout 60s; # 连接后端超时时间 + proxy_send_timeout 60s; # 发送请求超时时间 + proxy_read_timeout 60s; # 读取响应超时时间 + + # 缓冲配置 + proxy_buffering on; # 开启响应缓冲,减轻后端压力 + proxy_buffer_size 4k; # 响应头缓冲区大小 + proxy_buffers 8 4k; # 响应体缓冲区数量和大小 + } + + # 静态文件直接由 Nginx 提供(可选,提升性能) + location /static/ { + alias /data/apps/static/; # 映射到容器内的静态文件目录 + expires 7d; # 浏览器缓存 7 天 + access_log off; # 关闭静态文件访问日志 + } + + # 上传文件直接由 Nginx 提供(可选) + location /uploads/ { + alias /data/apps/uploads/; # 映射到容器内的上传文件目录 + expires 1d; # 浏览器缓存 1 天 + access_log off; + } + + # 安全配置:拒绝访问隐藏文件 + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } +} +``` + +:::tip HTTPS 配置 +生产环境建议启用 HTTPS。可使用 Let's Encrypt 免费证书: + +```bash +# 安装 certbot +apt install certbot python3-certbot-nginx + +# 申请证书并自动配置 Nginx +certbot --nginx -d your-domain.com +``` +::: + +### 4. 限制端口暴露 + +生产环境建议不暴露 MySQL 和 Redis 端口到宿主机: + +```yaml +# docker-compose.yml +mysql: + # ports: # 注释掉或删除 + # - "3306:3306" # 生产环境不暴露 + ... + +redis: + # ports: # 注释掉或删除 + # - "6379:6379" # 生产环境不暴露 + ... +``` + +### 5. 配置日志轮转 + +```yaml +# docker-compose.yml +services: + app: + logging: + driver: "json-file" + options: + max-size: "500m" # 单个日志文件最大 500MB + max-file: "10" # 最多保留 10 个日志文件 +``` + +## 常见问题 + +### 容器启动失败 + +```bash +# 查看容器日志 +docker compose logs app + +# 常见原因: +# 1. .env 配置错误(数据库密码、Redis 密码) +# 2. 端口被占用(修改 docker-compose.yml 中的端口映射) +# 3. 磁盘空间不足 +# 4. JWT_SECRET 未配置或过短 +``` + +### 数据库连接失败 + +``` +1. 确认 MySQL 容器已启动:docker compose ps +2. 确认 .env 中 DB_HOST=mysql(Docker 服务名称,不是 127.0.0.1) +3. 确认 DB_PASSWORD 与 MYSQL_ROOT_PASSWORD 一致 +4. 确认 MySQL 已就绪:docker compose logs mysql +``` + +### Redis 连接失败 + +``` +1. 确认 Redis 容器已启动:docker compose ps +2. 确认 .env 中 REDIS_HOST=redis(Docker 服务名称,不是 127.0.0.1) +3. 确认 REDIS_PASSWORD 与 docker-compose.yml 中的密码一致 +``` + +### 容器内时区不对 + +Dockerfile 已设置 `TZ=Asia/Shanghai`,如果仍有时区问题: + +```bash +# 进入容器检查时区 +docker compose exec app date + +# 手动设置时区(docker-compose.yml) +environment: + - TZ=Asia/Shanghai +``` + +## 总结 + +Docker 部署通过 Docker Compose 一键启动应用、MySQL、Redis 三个服务。多阶段构建减小镜像体积,命名卷保证数据持久化。生产环境建议关闭调试模式、使用强密钥、配置 Nginx 反向代理、限制端口暴露、配置日志轮转。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume1/env-vars.md b/DjangoAdmin_Tornado_EleVue/zh/volume1/env-vars.md new file mode 100644 index 0000000..960ba41 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume1/env-vars.md @@ -0,0 +1,196 @@ +# 环境变量速查表 + +本章节汇总了项目 `.env` 文件中所有可配置的环境变量,按功能分类整理。所有变量均可在 `.env.example` 中找到示例。 + +:::tip 温馨提示 +1. `.env.example` 是环境变量模板文件,已提交到版本控制。真实 `.env` 文件已被 `.gitignore` 忽略。 +2. 首次使用请执行 `cp .env.example .env` 复制模板后修改。 +3. 带有 `必填` 标记的变量必须设置,否则应用无法正常启动。 +::: + +## 基础配置 + +| 变量名 | 默认值 | 说明 | +|:----|:----|:----| +| TORNADO_NAME | Tornado+EleVue旗舰版 | 应用名称,用于界面显示和系统标识 | +| TORNADO_VERSION | v3.0.0 | 应用版本号 | +| TORNADO_HOST | 127.0.0.1 | 应用监听地址。`127.0.0.1` 仅本地访问;容器/生产建议 `0.0.0.0` | +| TORNADO_PORT | 8041 | 应用监听端口 | +| TORNADO_APP | app.py | 应用启动文件名 | +| TORNADO_ENV | development | 运行环境:`development` / `production` / `testing` | +| TORNADO_DEBUG | True | 是否开启调试模式。`True` 时输出详细错误堆栈和 SQL 日志,生产环境务必设为 `False` | +| TORNADO_DEMO | False | 是否开启演示模式。`True` 时所有写操作(add/update/delete/status/batchDelete)返回"演示环境,暂无操作权限" | +| TORNADO_CORS_ORIGINS | (空) | CORS 允许的源白名单,逗号分隔。留空则允许所有来源 | +| TORNADO_TRUST_PROXY | False | 是否信任反向代理的 X-Forwarded-For 头。为 True 时取 XFF 最左侧 IP 作为客户端真实 IP | +| TORNADO_FILE_URL | (空) | 文件访问域名,用于拼接上传文件的访问 URL | +| UPLOAD_ALLOWED_EXTS | .jpg,.jpeg,.png,...,.zip | 允许上传的文件扩展名,逗号分隔(带点号) | +| UPLOAD_MAX_SIZE_MB | 10 | 上传文件大小上限(MB) | + +:::tip 安全提示 +生产环境务必设置 `TORNADO_DEBUG=False`,调试模式会输出详细的错误堆栈信息,可能泄漏内部实现细节。 +::: + +## 数据库配置 + +| 变量名 | 默认值 | 说明 | +|:----|:----|:----| +| DB_DRIVER | mysql | 数据库驱动。可选:`mysql` / `postgresql` / `mssql` / `oracle` / `sqlite` | +| DB_HOST | 127.0.0.1 | 数据库地址(`sqlite` 时忽略) | +| DB_PORT | 3306 | 数据库端口(`sqlite` 时忽略) | +| DB_DATABASE | djangoadmin.tornado.elevue | 数据库名称。`sqlite` 时作为数据库文件完整路径 | +| DB_USERNAME | root | 数据库账号(`sqlite` 时忽略) | +| DB_PASSWORD | (空) | 数据库密码(`sqlite` 除外) | +| DB_PREFIX | tornado_ | 数据表前缀,所有业务表名自动添加此前缀 | +| DB_DEBUG | False | 是否开启 SQL 调试日志(对应 SQLAlchemy `echo` 参数)。生产环境务必设为 `False` | +| READ_DATABASE_URL | (空) | 读库地址(读写分离,可选;留空则读写同库) | + +:::tip 多数据库支持 +`DB_DRIVER` 支持别名:`postgres` → `postgresql`、`sqlserver` → `mssql`、`sqlite3` → `sqlite`。切换驱动后需安装对应的 Python 驱动包(`poetry install -E postgresql` / `-E mssql` / `-E oracle`)。 +::: + +## Redis 缓存配置 + +| 变量名 | 默认值 | 说明 | +|:----|:----|:----| +| REDIS_HOST | 127.0.0.1 | Redis 服务地址 | +| REDIS_PORT | 6379 | Redis 服务端口 | +| REDIS_PASSWORD | (空) | Redis 密码。`REDIS_AUTH=True` 时必填 | +| REDIS_INDEX | 0 | Redis 库索引(0-15) | +| REDIS_AUTH | True | 是否启用 Redis 认证。设为 `False` 时无需密码 | +| REDIS_DECODE_RESPONSES | True | 是否自动解码响应(bytes → str) | + +:::tip 温馨提示 +Redis 用于存储验证码、JWT 令牌黑名单和登录防爆破数据,是项目正常运行的必需服务。 +::: + +## JWT 令牌配置(必填) + +| 变量名 | 默认值 | 说明 | +|:----|:----|:----| +| JWT_SECRET | (空) | JWT 签名密钥,**必填**。至少 32 字节强随机值,留空或过弱应用会拒绝启动(fail-close) | +| JWT_EXPIRE_MINUTES | 20 | JWT 令牌过期时间(分钟) | +| LOGIN_FAIL_MAX | 5 | 登录失败次数上限,超过后锁定账号(防暴力破解) | +| LOGIN_LOCK_SECONDS | 300 | 登录锁定时间(秒) | + +:::danger 重要提示 +`JWT_SECRET` 是安全关键配置,必须设置。推荐使用以下命令生成: + +```bash +python -c "import secrets; print(secrets.token_urlsafe(48))" +``` + +生成后请妥善保管,泄漏会导致令牌被伪造。已知不安全的默认值(如空字符串、`change-me`、`secret` 等)会在启动时被拦截。 +::: + +## 验证码配置 + +| 变量名 | 默认值 | 说明 | +|:----|:----|:----| +| CAPTCHA_LENGTH | 6 | 验证码字符长度(4-6 位) | +| CAPTCHA_EXPIRE | 300 | 验证码过期时间(秒) | +| CAPTCHA_RATE_WINDOW | 60 | 验证码防刷窗口期(秒) | +| CAPTCHA_RATE_LIMIT | 20 | 窗口期内最大获取次数 | +| CAPTCHA_CHARS | 346789ABCDEFGHJKMNPQRTUVWXY | 验证码字符集(已去除易混淆字符) | +| CAPTCHA_IMAGE_WIDTH | 200 | 验证码图片宽度(像素) | +| CAPTCHA_IMAGE_HEIGHT | 60 | 验证码图片高度(像素) | +| CAPTCHA_FONT_SIZE | 30 | 验证码字体大小 | +| CAPTCHA_NOISE_LINES | 10 | 干扰线数量 | +| CAPTCHA_NOISE_DOTS | 60 | 噪点数量 | +| CAPTCHA_BYPASS_CODE | 618618 | 万能验证码(仅 TORNADO_DEMO=True 时生效) | + +## 邮件配置 + +| 变量名 | 默认值 | 说明 | +|:----|:----|:----| +| MAIL_MAILER | smtp | 邮件传输协议 | +| MAIL_SERVER | smtp.163.com | SMTP 服务器地址 | +| MAIL_PORT | 465 | SMTP 服务端口 | +| MAIL_USE_SSL | True | 是否启用 SSL 加密 | +| MAIL_USERNAME | (空) | 授权邮箱用户名 | +| MAIL_PASSWORD | (空) | 授权邮箱密码/授权码 | +| MAIL_ENCRYPTION | (空) | 邮件加密串(可选) | +| MAIL_FROM_NAME | (空) | 发件人显示名称 | +| MAIL_FROM_ADDRESS | (空) | 发件人邮箱地址 | + +:::tip 温馨提示 +邮件功能为预留扩展功能,暂未实现。 +::: + +## 日志配置 + +| 变量名 | 默认值 | 说明 | +|:----|:----|:----| +| LOG_LEVEL | INFO | 日志级别:DEBUG / INFO / WARNING / ERROR | +| LOG_DIR | logs | 日志输出目录 | + +## 环境变量配置示例 + +以下是一个开发环境的 `.env` 文件示例(基于 `.env.example` 模板修改): + +```bash +# ============================================================ +# 基础配置 +# ============================================================ +TORNADO_NAME=Tornado+EleVue旗舰版 +TORNADO_VERSION=v3.0.0 +TORNADO_HOST=127.0.0.1 +TORNADO_PORT=8041 +TORNADO_ENV=development +TORNADO_DEBUG=True +TORNADO_DEMO=False + +# ============================================================ +# 数据库配置 +# ============================================================ +DB_DRIVER=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=djangoadmin.tornado.elevue +DB_USERNAME=root +DB_PASSWORD=your_password +DB_PREFIX=tornado_ +DB_DEBUG=False + +# ============================================================ +# Redis 配置 +# ============================================================ +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 +REDIS_PASSWORD=your_redis_password +REDIS_INDEX=0 +REDIS_AUTH=True + +# ============================================================ +# JWT 配置(必填) +# ============================================================ +JWT_SECRET=your_generated_secret_key_here_at_least_32_bytes +JWT_EXPIRE_MINUTES=20 +LOGIN_FAIL_MAX=5 +LOGIN_LOCK_SECONDS=300 + +# ============================================================ +# 验证码配置 +# ============================================================ +CAPTCHA_LENGTH=6 +CAPTCHA_EXPIRE=300 + +# ============================================================ +# 上传配置 +# ============================================================ +UPLOAD_ALLOWED_EXTS=.jpg,.jpeg,.png,.gif,.bmp,.ico,.txt,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.mp3,.mp4,.zip,.rar,.7z +UPLOAD_MAX_SIZE_MB=10 +``` + +:::tip 配置来源 +所有配置变量在 `src/config/` 目录下的各模块中读取:`app.py`(基础配置)、`database.py`(数据库)、`auth.py`(JWT)、`redis.py`(Redis)、`captcha.py`(验证码)、`email.py`(邮件)。通过 `python-dotenv` 从 `.env` 文件加载。 +::: + +:::danger 安全提醒 +1. 真实 `.env` 文件包含敏感信息(数据库密码、JWT 密钥等),已被 `.gitignore` 忽略,请勿提交到版本控制。 +2. 生产环境务必设置 `TORNADO_DEBUG=False` 和 `DB_DEBUG=False`。 +3. 定期轮换 `JWT_SECRET` 密钥,提高系统安全性。 +::: + +## 总结 + +本章节汇总了项目所有可配置的环境变量,涵盖基础配置、数据库、Redis、JWT、验证码、上传、邮件和日志等功能模块。通过合理配置这些变量,可以灵活调整项目的运行参数,适应不同的开发、测试和生产环境需求。建议首次使用时以 `.env.example` 为模板,仅修改必需项,其余保持默认值即可快速启动项目。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume1/environment.md b/DjangoAdmin_Tornado_EleVue/zh/volume1/environment.md new file mode 100644 index 0000000..3aa1a13 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume1/environment.md @@ -0,0 +1,477 @@ +# 环境准备 + +本章节介绍在本地搭建项目所需的基础环境和软件安装。项目采用 `Python` + `Tornado` 后端、`Vue3` + `ElementPlus` 前端的分离架构,本地开发需准备以下软件。 + +安装软件:`Python(3.12+)`、`MySQL(8.0+)`、`Redis(3.2+)`、`Node.js(22+)`、`pnpm` 等。 + +:::tip 温馨提示 +1. 部分软件安装后需要设置本地环境变量,便于在终端中使用命令。 +2. `Node.js` 安装后还需另外安装 `pnpm` 包管理器,要求 Node.js 22+ 版本以上。 +3. `MySQL 8.0+` 安装启动服务后,可以安装可视化工具 `Navicat` 并连接数据库。 +4. 非 Java/Spring 技术栈,无需安装 JDK 和 Maven。 +::: + +## 安装 Python + +`Python` 是后端的核心运行环境,`Tornado` 框架要求 `Python 3.12` 或更高版本。 + ++ 下载 Python + +访问 [Python 官方网站](https://www.python.org/downloads/),选择适合你操作系统的 `Python 3.12+` 版本(推荐最新稳定版)。 + +![](/volume1/environment/1.png) + +![](/volume1/environment/2.png) + +点击 `Download` 按钮,下载对应平台的安装包。 + ++ 安装 Python + +**Windows 系统** + +双击下载的 `.exe` 文件启动安装程序。 + +:::danger 重要提示 +安装时务必勾选 **`Add Python to PATH`** 选项,将 Python 添加到系统环境变量。否则后续命令无法识别 `python` 和 `pip` 指令。 +::: + +选择 `Customize installation`,确认以下选项已勾选: + +``` +pip - Python 包管理器 +tcl/tk and IDLE - 图形界面支持(可选) +py launcher - Python 启动器 +``` + +点击 `Install` 开始安装,等待安装完成。 + +**macOS 系统** + +双击下载的 `.pkg` 文件,按照安装向导完成安装。macOS 通常自带 Python,但建议安装官方版本以确保版本一致。 + +**Linux 系统** + +大多数 Linux 发行版自带 Python,但版本可能较低,建议通过包管理器安装 `Python 3.12+`。 + +Ubuntu / Debian: + +```bash +# 更新包索引 +sudo apt update + +# 安装 Python 3.12 及 pip +sudo apt install -y python3.12 python3.12-venv python3-pip + +# 设置 python3 指向 3.12(可选,部分系统默认已是) +sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1 +``` + +CentOS / RHEL / Fedora: + +```bash +# CentOS / RHEL 8+ / Fedora +sudo dnf install -y python3.12 python3.12-pip + +# CentOS 7(需先启用 EPEL 和 IUS 源) +sudo yum install -y epel-release +sudo yum install -y https://repo.ius.io/ius-release-el7.rpm +sudo yum install -y python312 python312-pip +``` + +:::tip 温馨提示 +1. 部分系统需使用 `python3` 和 `pip3` 命令而非 `python` 和 `pip`,可通过 `alias python=python3` 简化。 +2. 推荐使用 `python3 -m venv` 创建虚拟环境隔离项目依赖: + +```bash +python3 -m venv .venv +source .venv/bin/activate +``` +::: + ++ 验证安装 + +打开终端或命令提示符,输入以下命令确认 `Python` 和 `pip` 安装成功: + +```bash +# 查看 Python 版本 +python --version +Python 3.12.7 + +# 查看 pip 版本 +pip --version +pip 24.2 from ... (python 3.12) +``` + +:::tip 温馨提示 +如果显示版本号,说明 `Python` 和 `pip` 安装成功。推荐使用 `Python 3.12` 或更高版本。 +::: + ++ 配置 pip 镜像源(可选) + +国内网络环境建议配置 `pip` 镜像源,加速依赖包下载: + +```bash +# 设置阿里云镜像源 +pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ + +# 设置信任主机 +pip config set global.trusted-host mirrors.aliyun.com +``` + +## 安装 MySQL + +`MySQL` 是默认的关系型数据库,要求 `8.0` 或更高版本。项目同时支持 `PostgreSQL`、`SQL Server`、`SQLite`、`Oracle` 等数据库驱动。 + ++ 下载软件 + +访问 [MySQL 官方网站](https://dev.mysql.com/downloads/mysql/),选择适合你操作系统的 `MySQL 8.0+` 版本(推荐最新稳定版)。 + +![](/volume1/environment/3.png) + +点击 `Download` 按钮,选择合适的安装包下载。 + ++ 安装软件 + +**Windows 系统** + +双击下载的 `.msi` 文件启动安装向导,按照提示完成安装。安装过程中需要设置 `root` 用户密码,请牢记此密码,后续配置 `.env` 文件时需要使用。 + +**macOS 系统** + +推荐使用 `Homebrew` 安装: + +```bash +brew install mysql +brew services start mysql +``` + +**Linux 系统** + +Ubuntu / Debian: + +```bash +# 更新包索引 +sudo apt update + +# 安装 MySQL 8.0+ +sudo apt install -y mysql-server + +# 启动并设置开机自启 +sudo systemctl start mysql +sudo systemctl enable mysql + +# 安全初始化(设置 root 密码、移除匿名用户等) +sudo mysql_secure_installation +``` + +CentOS / RHEL / Fedora: + +```bash +# CentOS / RHEL 8+ / Fedora +sudo dnf install -y mysql-server + +# CentOS 7 +sudo yum install -y mysql-server + +# 启动并设置开机自启 +sudo systemctl start mysqld +sudo systemctl enable mysqld +``` + +:::tip 温馨提示 +1. CentOS / RHEL 8+ 默认安装 `mysql-server`(即 MySQL 8.0 社区版)。 +2. CentOS 7 安装后需通过 `grep 'temporary password' /var/log/mysqld.log` 获取临时 root 密码。 +3. 部分发行版可使用 `mariadb-server` 作为 MySQL 兼容替代,但建议使用官方 MySQL 以确保兼容性。 +::: + ++ 验证安装 + +打开命令提示符或终端,输入以下命令登录 `MySQL`: + +```bash +# 登录本地 MySQL 服务 +mysql -uroot -p + +# 输入密码后显示如下信息 +Enter password: **** +Welcome to the MySQL monitor. Commands end with ; or \g. +Your MySQL connection id is 14 +Server version: 8.0.41 MySQL Community Server - GPL + +mysql> +``` + +:::tip 温馨提示 +输入之前设置的 `root` 密码,成功登录后显示 `MySQL` 提示符即代表安装成功。 +::: + ++ 创建数据库(可选) + +在 `MySQL` 提示符中,可提前创建项目数据库: + +```sql +-- 创建数据库(UTF-8 编码) +CREATE DATABASE `djangoadmin.tornado.elevue` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + +-- 查看数据库 +SHOW DATABASES; +``` + +:::tip 温馨提示 +也可以不手动建库,后续执行 `python scripts/init_db.py` 会自动创建数据库并导入初始数据。 +::: + +## 安装 Redis + +`Redis` 是必需的缓存服务,用于验证码存储、JWT 令牌黑名单、登录限流等功能。要求 `3.2` 或更高版本。 + ++ 下载软件 + +**Windows 系统** + +访问 [Microsoft Archive for Redis](https://github.com/microsoftarchive/redis/tags),下载适用于 Windows 的安装包(推荐 `Redis-x64-3.2.100.msi`)。 + +**macOS 系统** + +```bash +brew install redis +brew services start redis +``` + +**Linux 系统** + +Ubuntu / Debian: + +```bash +sudo apt update +sudo apt install -y redis-server + +# 启动并设置开机自启 +sudo systemctl start redis-server +sudo systemctl enable redis-server +``` + +CentOS / RHEL / Fedora: + +```bash +# CentOS / RHEL 8+ / Fedora +sudo dnf install -y redis + +# CentOS 7 +sudo yum install -y redis + +# 启动并设置开机自启 +sudo systemctl start redis +sudo systemctl enable redis +``` + ++ 验证安装 + +在终端或命令提示符中,执行以下命令启动客户端并测试: + +```bash +# 启动 Redis 客户端 +redis-cli + +# 测试连接 +127.0.0.1:6379> ping +PONG + +# 设置键值对 +127.0.0.1:6379> set name django_admin +OK + +# 获取键值 +127.0.0.1:6379> get name +"django_admin" +``` + +:::tip 温馨提示 +输入 `ping` 命令,如果返回 `PONG`,说明 Redis 客户端已成功连接到服务器。 +::: + +:::tip 常见问题与解决方法 +1. 服务无法启动:确保 Redis 安装目录正确,并且具有读取权限。检查端口 `6379` 是否被占用。 +2. 连接被拒绝:确保 Redis 服务器正在运行。验证防火墙设置,确保 Redis 端口(默认 `6379`)允许通过。 +::: + +## 安装 Node.js + +`Node.js` 是前端项目的运行环境,要求 `22` 或更高版本。前端基于 `Vue3` + `ElementPlus` 构建。 + ++ 下载 Node.js + +访问 [Node.js 官方网站](https://nodejs.org/zh-cn),选择 `LTS` 版本(推荐 `v22.x` 或更高),下载适合你操作系统的安装包。 + +![](/volume1/environment/4.png) + ++ 安装 Node.js + +**Windows 系统** + +双击下载的 `.msi` 文件启动安装程序,按照提示完成安装。安装时建议勾选以下选项: + +``` +Automatically install the necessary tools - 自动安装所需工具 +Add to PATH - 将 Node.js 添加到系统环境变量 +``` + +**macOS 系统** + +双击下载的 `.pkg` 文件,按照安装向导完成安装。 + +**Linux 系统** + +推荐使用 `nvm`(Node Version Manager)安装,便于管理多版本: + +```bash +# 安装 nvm +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash + +# 重新加载 shell 配置 +source ~/.bashrc + +# 安装 Node.js 22+ +nvm install 22 + +# 设置为默认版本 +nvm alias default 22 +``` + +或通过 NodeSource 源直接安装: + +```bash +# 添加 NodeSource 源(以 Node.js 22 为例) +curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - + +# Ubuntu / Debian +sudo apt install -y nodejs + +# CentOS / RHEL / Fedora +sudo dnf install -y nodejs +``` + ++ 验证安装 + +打开终端或命令提示符,输入以下命令: + +```bash +# 查看 Node.js 版本 +node -v +v22.14.0 + +# 查看 npm 版本 +npm -v +10.9.2 +``` + +:::tip 温馨提示 +如果显示版本号,说明 `Node.js` 和 `npm` 安装成功。如需同时管理多个 Node 版本,可以使用 `nvm` 工具。 +::: + +## 安装 pnpm + +`pnpm` 是推荐的前端包管理器,相比 `npm` 和 `yarn` 具有更快的安装速度和更小的磁盘占用。 + ++ 安装 pnpm + +通过 `npm` 全局安装 `pnpm`: + +```bash +npm install pnpm -g +``` + ++ 验证安装 + +```bash +# 查看 pnpm 版本 +pnpm -v +9.15.0 +``` + +:::tip 温馨提示 +`pnpm` 安装成功后,后续进入前端项目目录即可使用 `pnpm install` 安装依赖。 +::: + ++ 设置镜像源(可选) + +国内网络环境建议配置 `pnpm` 镜像源,加速依赖包下载: + +```bash +# 设置阿里云镜像源 +pnpm config set registry https://registry.npmmirror.com + +# 查看当前镜像源 +pnpm config get registry +``` + +## 安装 Git + +`Git` 是必需的版本控制工具,用于代码的拉取和管理。 + ++ 下载 Git + +访问 [Git 官方网站](https://git-scm.com/downloads),下载适合你操作系统的安装包。 + ++ 安装 Git + +**Windows 系统** + +双击下载的 `.exe` 文件,按照安装向导完成安装。安装过程中保持默认选项即可。 + +**macOS 系统** + +```bash +# 使用 Homebrew 安装 +brew install git +``` + +**Linux 系统** + +```bash +# Ubuntu / Debian +sudo apt update +sudo apt install -y git + +# CentOS / RHEL / Fedora +sudo dnf install -y git +``` + ++ 验证安装 + +```bash +# 查看 Git 版本 +git --version +git version 2.43.0 +``` + +## 环境验证清单 + +完成以上所有软件安装后,打开终端依次执行以下命令,确认所有环境就绪: + +```bash +# 1. Python 环境 +python --version # 期望: Python 3.12+ +pip --version # 期望: pip 对应 Python 版本 + +# 2. 数据库 +mysql --version # 期望: mysql Ver 8.0+ + +# 3. 缓存 +redis-cli --version # 期望: redis-cli 3.2+ + +# 4. 前端运行时 +node -v # 期望: v22+ +pnpm -v # 期望: 12.x+ + +# 5. 版本控制 +git --version # 期望: git version 2.x+ +``` + +:::tip 全部就绪 +以上命令均能正确输出版本号,即表示本地开发环境已准备完毕,可以进入下一步:[后端启动](/zh/volume1/backend)。 +::: + +## 总结 + +本章节介绍了本地开发环境所需的全部基础软件及其安装配置步骤。通过以上步骤,你已经完成了 `Python`、`MySQL`、`Redis`、`Node.js`、`pnpm`、`Git` 的安装和验证。这些是运行项目的基础环境,后续章节将基于这些环境进行项目的启动和调试。如果在安装过程中遇到问题,可参考各软件的官方文档或查阅 [常见问题FAQ](/zh/volume1/faq) 章节。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume1/faq.md b/DjangoAdmin_Tornado_EleVue/zh/volume1/faq.md new file mode 100644 index 0000000..5d3338e --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume1/faq.md @@ -0,0 +1,424 @@ +# 常见问题FAQ + +本章节整理了项目开发和部署过程中常见的问题及解决方案,帮助你快速定位和解决问题。 + +:::tip 温馨提示 +遇到问题时,建议先查看终端日志输出和浏览器控制台(F12)的错误信息,通常能快速定位问题原因。 +::: + +## 1. 端口被占用 + +**现象** + +启动后端服务时报错: + +``` +OSError: [Errno 98] Address already in use +``` + +或: + +``` +ERROR: [Errno 10048] 通常每个套接字地址(协议/网络地址/端口)只允许使用一次 +``` + +**原因** + +端口 `8041`(或配置的其他端口)已被其他程序占用。 + +**解决方案** + +```bash +# Windows 查看端口占用 +netstat -ano | findstr 8041 +# 找到 PID 后终止进程 +taskkill /PID <进程ID> /F + +# Linux / macOS 查看端口占用 +lsof -i :8041 +# 终止进程 +kill -9 <进程ID> +``` + +或者修改 `.env` 中的端口号: + +```bash +TORNADO_PORT=8042 +``` + +:::tip 温馨提示 +前端开发服务器端口冲突同理,修改 `ui/.env.development` 中的 `VITE_PORT` 即可。 +::: + +## 2. 数据库连接失败 + +**现象** + +启动时报错: + +``` +pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on '127.0.0.1'") +``` + +或: + +``` +sqlalchemy.exc.OperationalError: (pymysql.err.OperationalError) (1045, "Access denied") +``` + +**排查步骤** + +``` +1. 检查 MySQL 服务是否已启动 +2. 检查 .env 中的 DB_HOST、DB_PORT、DB_USERNAME、DB_PASSWORD 是否正确 +3. 检查数据库用户是否有远程连接权限 +4. 检查防火墙是否放行了数据库端口 +``` + +**解决方案** + +```bash +# 测试数据库连接 +mysql -h127.0.0.1 -P3306 -uroot -p + +# 如果是权限问题,授权用户 +mysql -uroot -p +GRANT ALL PRIVILEGES ON `djangoadmin.tornado.elevue2`.* TO 'root'@'%' IDENTIFIED BY 'your_password'; +FLUSH PRIVILEGES; +``` + +:::tip 温馨提示 +确认 `.env` 中的 `DB_PASSWORD` 没有多余的空格或引号。密码中的特殊字符可能需要转义。 +::: + +## 3. Redis 连接失败 + +**现象** + +启动时报错: + +``` +redis.exceptions.ConnectionError: Error connecting to 127.0.0.1:6379 +``` + +或: + +``` +redis.exceptions.AuthenticationError: Authentication required +``` + +**排查步骤** + +``` +1. 检查 Redis 服务是否已启动 +2. 检查 .env 中的 REDIS_HOST、REDIS_PORT、REDIS_PASSWORD 是否正确 +3. 检查 REDIS_AUTH 配置是否与 Redis 实际配置一致 +``` + +**解决方案** + +```bash +# 测试 Redis 连接 +redis-cli -h 127.0.0.1 -p 6379 -a your_password ping + +# Windows 启动 Redis 服务 +net start Redis + +# Linux 启动 Redis 服务 +sudo systemctl start redis + +# macOS 启动 Redis 服务 +brew services start redis +``` + +:::tip 温馨提示 +如果 Redis 未设置密码,请将 `.env` 中的 `REDIS_AUTH` 设为 `False`。 +::: + +## 4. pip install 安装失败 + +**现象** + +执行 `pip install -r requirements.txt` 时报错: + +``` +ERROR: Could not find a version that satisfies the requirement xxx +``` + +或: + +``` +ERROR: Failed building wheel for xxx +``` + +**解决方案** + +```bash +# 方案一:使用国内镜像源 +pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ + +# 方案二:升级 pip 后重试 +python -m pip install --upgrade pip +pip install -r requirements.txt + +# 方案三:配置全局镜像源 +pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ +pip config set global.trusted-host mirrors.aliyun.com +``` + +:::tip 温馨提示 +Windows 用户如果遇到 C 编译错误(如安装 `mysqlclient` 时),可尝试安装 Visual C++ Build Tools,或使用预编译的 wheel 包。 +::: + +## 5. JWT_SECRET 未配置 + +**现象** + +启动时报错: + +``` +ValueError: JWT_SECRET must be set +``` + +或类似的安全配置错误。 + +**原因** + +`.env` 文件中 `JWT_SECRET` 变量为空或未设置。JWT 签名密钥是安全关键配置,应用在 fail-close 模式下会拒绝启动。 + +**解决方案** + +```bash +# 生成 JWT 密钥 +python -c "import secrets; print(secrets.token_urlsafe(48))" + +# 将生成的密钥写入 .env +JWT_SECRET=your_generated_secret_key_here +``` + +:::danger 重要提示 +`JWT_SECRET` 至少需要 32 字节的强随机值。请勿使用简单字符串(如 `123456` 或 `secret`)作为密钥。 +::: + +## 6. pnpm install 安装失败 + +**现象** + +执行 `pnpm install` 时报错: + +``` +ERR_PNPM_NO_MATCHING_VERSION +``` + +或: + +``` +GET https://registry.npmjs.org/xxx error +``` + +**解决方案** + +```bash +# 方案一:切换淘宝镜像源 +pnpm config set registry https://registry.npmmirror.com +pnpm install + +# 方案二:清除缓存后重试 +pnpm store prune +rm -rf node_modules +pnpm install + +# 方案三:使用 --shamefully-hoist 模式 +pnpm install --shamefully-hoist +``` + +:::tip 温馨提示 +如果网络环境受限,可以尝试使用 VPN 或配置代理: + +```bash +pnpm config set proxy http://127.0.0.1:7890 +pnpm config set https-proxy http://127.0.0.1:7890 +``` +::: + +## 7. API 请求返回 404 + +**现象** + +调用接口时返回: + +```json +{ + "detail": "Not Found" +} +``` + +**原因** + +可能是请求路径不正确,或后端路由未正确注册。 + +**排查步骤** + +``` +1. 检查请求路径是否以 /api/v1/ 开头 +2. 检查 Swagger UI(http://127.0.0.1:8041/docs)中是否存在该接口 +3. 检查前端代理配置中的后端地址是否正确 +``` + +**解决方案** + +```bash +# 测试后端服务是否正常 +curl http://127.0.0.1:8041/api/v1/captcha + +# 检查前端代理配置 +# ui/.env.development +VITE_PROXY=[["/api","http://127.0.0.1:8041/api"]] +``` + +:::tip 温馨提示 +所有业务接口都在 `/api/v1/` 前缀下。登录和验证码接口在 `/api/v1/` 下无需认证,其他接口需要携带 JWT Token。 +::: + +## 8. 验证码图片不显示 + +**现象** + +登录页面验证码图片区域空白,或显示加载失败图标。 + +**排查步骤** + +``` +1. 打开浏览器控制台(F12),查看 Network 标签中的请求状态 +2. 检查 /api/v1/captcha 接口是否返回正常 +3. 检查 Redis 服务是否正常(验证码存储在 Redis 中) +``` + +**解决方案** + +```bash +# 1. 测试验证码接口 +curl http://127.0.0.1:8041/api/v1/captcha + +# 2. 测试 Redis 连接 +redis-cli ping +# 应返回 PONG + +# 3. 检查验证码字体文件是否存在 +ls static/fonts/Vera.ttf + +# 4. 如果字体文件缺失,检查 .env 中的 CAPTCHA_FONT_PATH 配置 +``` + +:::tip 温馨提示 +验证码生成依赖 `Pillow` 库和字体文件。如果字体文件缺失,可从项目仓库重新拉取,或修改 `CAPTCHA_FONT_PATH` 指向系统字体。 +::: + +## 9. 接口返回 403 权限不足 + +**现象** + +调用接口返回: + +```json +{ + "code": 1, + "data": null, + "msg": "权限不足", + "ok": false +} +``` + +**原因** + +当前登录用户没有该接口的访问权限。项目采用 RBAC 权限模型,每个接口对应一个权限字符串(如 `sys:user:list`)。 + +**解决方案** + +``` +1. 使用 admin 账号登录(ID=1 的管理员绕过所有权限检查) +2. 检查当前用户的角色是否包含该接口的权限 +3. 在系统管理 > 菜单管理中,确认接口对应的权限节点已正确配置 +4. 在系统管理 > 角色管理中,确认角色已分配该权限 +``` + +:::tip 温馨提示 +`admin` 用户(ID=1)是超级管理员,拥有全部权限,不受权限检查限制。开发调试时建议使用 `admin` 账号。 +::: + +## 10. 文件上传失败 + +**现象** + +上传文件时返回错误: + +```json +{ + "code": 1, + "data": null, + "msg": "文件上传失败", + "ok": false +} +``` + +或: + +``` +413 Request Entity Too Large +``` + +**排查步骤** + +``` +1. 检查上传文件扩展名是否在 ALLOWED_EXTS 白名单中 +2. 检查文件大小是否超过 UPLOAD_MAX_SIZE_MB 限制 +3. 检查上传目录是否有写入权限 +4. 检查 .env 中的 TORNADO_FILE_URL 配置是否正确 +``` + +**解决方案** + +```bash +# .env 中的上传配置 +UPLOAD_ALLOWED_EXTS=.jpg,.jpeg,.png,.gif,.bmp,.ico,.txt,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.mp3,.mp4,.zip,.rar,.7z +UPLOAD_MAX_SIZE_MB=10 +TORNADO_FILE_URL=http://file.tornado.elevue +``` + +:::tip 温馨提示 +1. 如需上传更多文件类型,在 `UPLOAD_ALLOWED_EXTS` 中添加对应扩展名(带点号,逗号分隔)。 +2. 如需上传更大文件,增大 `UPLOAD_MAX_SIZE_MB` 值。 +3. 确保项目根目录下的 `uploads/` 目录存在且有写入权限。 +::: + +## 快速自检清单 + +遇到问题时,按以下清单逐项检查: + +| 检查项 | 命令 | 期望结果 | +|:----|:----|:----| +| Python 版本 | `python --version` | 3.12+ | +| pip 依赖 | `poetry show tornado` | tornado 已安装 | +| MySQL 服务 | `mysql -uroot -p -e "SELECT 1"` | 返回 1 | +| Redis 服务 | `redis-cli ping` | PONG | +| .env 文件 | `cat .env \| grep JWT_SECRET` | 非空值 | +| 后端服务 | `curl http://127.0.0.1:8041/api/v1/captcha` | 返回 JSON | +| Node.js 版本 | `node -v` | v22+ | +| pnpm 版本 | `pnpm -v` | 11.x+ | +| 前端服务 | 浏览器访问 `http://localhost:8001` | 显示登录页 | + +:::tip 温馨提示 +如果以上检查项均正常但仍无法运行,建议查看完整错误日志: + +```bash +# 后端日志 +python app.py 2>&1 | tee backend.log + +# 前端日志 +cd ui && pnpm dev 2>&1 | tee frontend.log +``` +::: + +## 总结 + +本章节整理了项目开发中最常见的 10 个问题及其解决方案,涵盖环境配置、服务连接、依赖安装、权限控制和文件上传等方面。遇到问题时,建议先查看终端日志和浏览器控制台的错误信息,结合本章节的排查步骤逐一排除。如果问题仍未解决,可以查阅项目源码或联系开发团队获取支持。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume1/first-api.md b/DjangoAdmin_Tornado_EleVue/zh/volume1/first-api.md new file mode 100644 index 0000000..f6152e8 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume1/first-api.md @@ -0,0 +1,256 @@ +# 第一个接口调试 + +本章节介绍如何使用 `curl` 或 `Postman` 等工具调试后端 API 接口,通过完整的登录流程(获取验证码 → 登录 → 获取令牌 → 调用业务接口)帮助你快速理解项目的接口调用方式。 + +:::tip 温馨提示 +开始调试前,请确保已完成以下步骤: +1. 后端服务已启动(参见 [后端启动](/zh/volume1/backend)) +2. 数据库已初始化(参见 [数据库初始化](/zh/volume1/database)) +3. Redis 服务已启动 +::: + +## 接口调试工具 + +本项目后端基于 Tornado 框架,不提供内置的 Swagger 文档。推荐使用以下工具进行接口调试: + +| 工具 | 说明 | +|:-----|:-----| +| `curl` | 命令行 HTTP 客户端,适合快速测试 | +| `Postman` | 图形化 API 调试工具,支持集合管理和环境变量 | +| 浏览器开发者工具 | 适合调试 GET 请求 | + +## 第一步:获取验证码 + +登录前需要先获取图形验证码: + +```bash +curl http://127.0.0.1:8041/api/v1/captcha +``` + ++ 响应示例 + +```json +{ + "code": 0, + "data": { + "captcha": "data:image/png;base64,iVBORw0KG...", + "key": "sl1dhi-ejqV2PWH8tYDE1pVNaRiccsrQ" + }, + "msg": "操作成功", + "ok": true +} +``` + +:::tip 温馨提示 +响应中的 `key` 是验证码的唯一标识,后续登录时需要使用。`captcha` 是 Base64 编码的验证码图片,可以在浏览器中直接查看。 +::: + ++ 记录关键信息 + +``` +key: sl1dhi-ejqV2PWH8tYDE1pVNaRiccsrQ +验证码图片中的文字: (查看图片获取) +``` + +## 第二步:登录获取 Token + +使用验证码和账号信息进行登录,获取 JWT 访问令牌: + +```bash +curl -X POST "http://127.0.0.1:8041/api/v1/login" \ + -H "Content-Type: application/json" \ + -d '{ + "username": "admin", + "password": "123456", + "code": "图片中的验证码文字", + "key": "第一步获取的key" + }' +``` + ++ 成功响应 + +```json +{ + "code": 0, + "data": { + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + }, + "msg": "登录成功", + "ok": true +} +``` + ++ 失败响应示例 + +```json +{ + "code": 1, + "data": null, + "msg": "验证码错误或已过期", + "ok": false +} +``` + +:::danger 常见登录失败原因 +| 错误信息 | 原因 | 解决方式 | +|:----|:----|:----| +| 验证码错误或已过期 | 验证码输入错误或超过 5 分钟有效期 | 重新获取验证码 | +| 账号或密码错误 | 用户名或密码不正确 | 使用默认账号 `admin/123456` | +| 账号已被锁定 | 连续登录失败超过 5 次 | 等待 5 分钟后重试 | +| 登录失败次数过多 | 同一 IP+用户名连续失败 | 等待锁定时间后重试 | +::: + ++ 记录 Token + +``` +token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +``` + +:::tip 温馨提示 +Token 是后续所有认证接口的通行证,请妥善保存。默认有效期为 20 分钟(可通过 `.env` 中的 `JWT_EXPIRE_MINUTES` 修改)。 +::: + +## 第三步:使用 Token 调用业务接口 + +Token 获取后,在后续请求的 `Authorization` 头中携带即可调用需要认证的接口: + +```bash +# 获取用户列表 +curl -X GET "http://127.0.0.1:8041/api/v1/user/page?page=1&limit=10" \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +``` + ++ 成功响应 + +```json +{ + "code": 0, + "data": { + "list": [ + { + "id": 1, + "username": "admin", + "realname": "超级管理员", + "status": 0, + "dept_id": 1, + "create_time": "2024-01-01 00:00:00" + } + ], + "total": 1 + }, + "msg": "操作成功", + "ok": true +} +``` + +## 接口调用流程图 + +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ ┌──────────────┐ +│ 获取验证码 │───▶│ 用户登录 │───▶│ 携带 Token 请求 │───▶│ 业务数据响应 │ +│ GET /captcha │ │ POST /login │ │ Authorization: │ │ GET/POST │ +└──────────────┘ └──────────────┘ │ Bearer │ └──────────────┘ + └──────────────────┘ + │ │ │ + ▼ ▼ ▼ + key JWT Token 业务数据 + captcha (20分钟有效) +``` + +## 更多接口示例 + +```bash +# 获取验证码 +curl http://127.0.0.1:8041/api/v1/captcha + +# 登录 +curl -X POST "http://127.0.0.1:8041/api/v1/login" \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"123456","code":"验证码","key":"key值"}' + +# 健康检查(无需认证) +curl http://127.0.0.1:8041/api/v1/health + +# 获取用户列表(需要认证) +curl -X GET "http://127.0.0.1:8041/api/v1/user/page?page=1&limit=10" \ + -H "Authorization: Bearer " + +# 刷新 Token +curl -X POST "http://127.0.0.1:8041/api/v1/token/refresh" \ + -H "Authorization: Bearer " + +# 获取字典下拉数据(需要认证) +curl -X GET "http://127.0.0.1:8041/api/v1/dict/data/link_type" \ + -H "Authorization: Bearer " +``` + +## API 响应格式说明 + +所有接口统一返回以下 JSON 格式: + +```json +// 成功响应 +{ + "code": 0, + "data": {}, + "msg": "操作成功", + "ok": true +} + +// 失败响应 +{ + "code": 1, + "data": null, + "msg": "错误信息", + "ok": false +} + +// 分页响应 +{ + "code": 0, + "data": { + "list": [], + "total": 100 + }, + "msg": "操作成功", + "ok": true +} +``` + +:::tip HTTP 状态码 +业务成功时 HTTP 状态码为 `200`,通过 `code` 字段区分业务状态:`0` 表示成功,非 `0` 表示失败。认证失败(401)、权限不足(403)等会返回对应的 HTTP 状态码。 +::: + +## 常见问题 + ++ 接口返回 401 Unauthorized + +``` +原因:未提供 Token 或 Token 已过期 +解决:重新登录获取 Token,检查 Authorization 头格式是否正确(Bearer ) +``` + ++ 接口返回 403 Forbidden + +``` +原因:当前用户无该接口的访问权限 +解决:使用 admin 账号(ID=1 拥有全部权限),或检查角色权限配置 +``` + ++ Token 过期 + +``` +原因:Token 超过有效期(默认 20 分钟) +解决:重新登录获取新 Token,或调用 POST /api/v1/token/refresh 刷新 +``` + ++ 接口返回演示模式限制 + +``` +原因:.env 中 TORNADO_DEMO=True,写操作被禁止 +解决:开发环境设置 TORNADO_DEMO=False +``` + +## 总结 + +本章节通过完整的登录流程演示了如何调试后端 API 接口:获取验证码 → 登录获取 Token → 携带 Token 调用业务接口。通过这个流程,你已经掌握了项目接口的基本调用方式和认证机制。如果遇到问题,请参见 [常见问题FAQ](/zh/volume1/faq) 章节。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume1/frontend.md b/DjangoAdmin_Tornado_EleVue/zh/volume1/frontend.md new file mode 100644 index 0000000..4d99d2c --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume1/frontend.md @@ -0,0 +1,236 @@ +# 前端启动 + +本章节介绍如何将前端项目安装依赖并启动开发服务器。前端基于 `Vue3` + `ElementPlus` + `TypeScript`+`Vite` 构建,采用组件化开发模式,支持热更新和代理配置。 + +:::tip 温馨提示 +启动前端前,请确保已完成 [环境准备](/zh/volume1/environment) 章节中的 `Node.js` 和 `pnpm` 安装,并已启动后端服务(参见 [后端启动](/zh/volume1/backend))。 +::: + +## 前端目录结构 + +前端源码位于项目根目录的 `ui/` 文件夹中: + +``` +ui/ +├── public/ # 静态资源 +├── src/ +│ ├── api/ # 接口请求封装 +│ ├── assets/ # 资源文件(图片、样式等) +│ ├── components/ # 公共组件 +│ ├── layout/ # 布局组件 +│ ├── router/ # 路由配置 +│ ├── store/ # 状态管理(Pinia) +│ ├── utils/ # 工具函数 +│ ├── views/ # 页面视图 +│ ├── App.vue # 根组件 +│ └── main.js # 入口文件 +├── .env.development # 开发环境变量 +├── .env.production # 生产环境变量 +├── vite.config.ts # Vite 构建配置 +├── package.json # 依赖清单 +└── tsconfig.json # TypeScript 配置 +``` + +## 安装依赖 + +进入前端项目目录,使用 `pnpm` 安装依赖: + +```bash +# 进入前端目录 +cd ui + +# 安装依赖 +pnpm install +``` + +安装过程如下: + +``` +$ pnpm install +Lockfile is up to date, resolution step is skipped +Packages: +1200 +++++++++++++++++++++++++++++++++++++++++++++++++++ +Progress: resolved 1200, reused 1200, downloaded 0, added 1200, done + +Done in 15.2s +``` + +:::tip 温馨提示 +依赖安装完毕后,在 `ui/` 目录下会生成 `node_modules` 文件夹,此文件夹包含了项目全部的前端依赖包。首次安装可能需要较长时间,请耐心等待。 +::: + ++ 验证依赖安装 + +```bash +# 检查 Vue 版本 +npx vue --version + +# 检查 Vite 版本 +npx vite --version +``` + +:::tip 常见问题 +如果 `pnpm install` 失败,可尝试以下解决方案: + +```bash +# 清除缓存后重新安装 +pnpm store prune +pnpm install + +# 删除 node_modules 后重新安装 +rm -rf node_modules +pnpm install +``` +::: + +## 配置代理 + +前端开发服务器通过 `Vite` 代理将 API 请求转发到后端服务。代理配置位于 `vite.config.ts` 文件中。 + ++ 开发环境变量 + +前端开发环境的变量配置在 `.env.development` 文件中: + +```bash +# 开发服务器端口 +VITE_PORT = 8001 + +# 网站根目录 +VITE_PUBLIC_PATH = / + +# 跨域代理配置(指向后端服务地址) +VITE_PROXY=[["/api","http://127.0.0.1:8041/api"]] + +# 接口前缀 +VITE_GLOB_API_URL_PREFIX = /api +``` + +:::tip 代理说明 +`VITE_PROXY` 配置项中,`/api` 是前端请求的路径前缀,`http://127.0.0.1:8041/api` 是后端服务的实际地址。前端所有以 `/api` 开头的请求都会被代理到后端的 `8041` 端口。 +::: + ++ 修改代理地址 + +如果后端服务运行在不同的地址或端口,需要修改 `.env.development` 中的代理配置: + +```bash +# 示例:后端运行在 192.168.1.100 的 8041 端口 +VITE_PROXY=[["/api","http://192.168.1.100:8041/api"]] +``` + +## 启动开发服务器 + +确保后端服务已启动后,在 `ui/` 目录下执行以下命令: + +```bash +# 启动前端开发服务器 +pnpm dev +``` + +启动成功后,终端将输出类似以下信息: + +``` +$ vite + + VITE v3.2.10 ready in 943 ms + + ➜ Local: http://localhost:8001/ + ➜ Network: http://192.168.1.100:8001/ +``` + +:::tip 默认端口 +前端开发服务器默认运行在 `http://localhost:8001/`,可通过 `.env.development` 中的 `VITE_PORT` 参数修改端口。 +::: + +## 访问系统 + +在浏览器中打开以下地址: + +``` +http://localhost:8001/ +``` + +![](/demo/1.png) + ++ 默认登录账号 + +| 类型 | 账号 | 密码 | 说明 | +|:----|:----|:----|:----| +| 管理员 | `admin` | `123456` | 超管账号,默认拥有全部权限 | +| 普通用户 | `admin2` | `123456` | 普通账号,具有角色权限 | + +:::tip 登录验证 +1. 打开登录页面后,应能看到验证码图片正常显示,说明前后端通信正常。 +2. 输入账号 `admin` 和密码 `123456`,点击登录按钮。 +3. 成功登录后将进入系统主界面,表示前后端均已正确启动。 +::: + +:::danger 注意事项 +如果验证码图片无法显示,请检查: +1. 后端服务是否已启动(`http://127.0.0.1:8041`) +2. 代理配置是否正确(`.env.development` 中的 `VITE_PROXY`) +3. Redis 服务是否正常运行(验证码存储依赖 Redis) +::: + +## 前端构建与部署 + ++ 开发环境构建 + +```bash +# 开发环境打包 +pnpm build:dev +``` + ++ 生产环境构建 + +```bash +# 生产环境打包 +pnpm build +``` + +构建完成后,会在 `ui/` 目录下生成 `dist/` 文件夹,包含打包后的静态资源文件。将 `dist/` 目录部署到 `Nginx` 或其他 Web 服务器即可。 + ++ 预览构建结果 + +```bash +# 本地预览打包结果 +pnpm preview +``` + +## 常见问题 + ++ pnpm install 失败 + +```bash +# 清除 pnpm 缓存 +pnpm store prune + +# 使用淘宝镜像源 +pnpm config set registry https://registry.npmmirror.com +pnpm install +``` + ++ 端口被占用 + +如果启动时报端口冲突,可修改 `.env.development` 中的 `VITE_PORT`: + +```bash +VITE_PORT = 8002 +``` + ++ API 请求 404 + +检查后端服务是否启动,以及 `.env.development` 中的 `VITE_PROXY` 代理地址是否与后端实际地址一致。 + ++ 验证码不显示 + +``` +1. 确认后端服务已启动 +2. 确认 Redis 服务已启动并可连接 +3. 检查浏览器控制台(F12)是否有跨域错误 +4. 检查 .env 中 CORS_ALLOWED_ORIGINS 是否包含前端地址 +``` + +## 总结 + +本章节介绍了前端项目的完整启动流程:安装依赖 → 配置代理 → 启动开发服务器 → 访问登录页面。通过以上步骤,你已经成功将前端应用运行在本地 `8001` 端口,并使用默认账号 `admin/123456` 登录系统。下一步可以进入 [第一个接口调试](/zh/volume1/first-api) 章节,学习如何使用 Swagger UI 调试后端 API 接口。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume2/code-list.md b/DjangoAdmin_Tornado_EleVue/zh/volume2/code-list.md new file mode 100644 index 0000000..a9f8039 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume2/code-list.md @@ -0,0 +1,332 @@ +# 完整代码清单 + +本章汇总友链(Link)模块涉及的全部文件,便于开发时快速查阅和复制。 + +## 后端文件清单 + +### 1. 数据模型 `src/modules/link/models.py` + +```python +# +====================================================================== +# | 模块: 友链数据模型层 +# | 说明: 定义友链表结构,继承 BaseModel 获得通用字段与序列化能力 +# +====================================================================== +"""友链数据模型。""" + +from sqlalchemy import Column, Integer, String, text + +from config.database import DB_PREFIX +from core.base_model import BaseModel + + +class Link(BaseModel): + """友情链接模型""" + __tablename__ = DB_PREFIX + "link" + __table_args__ = {"comment": "友情链接表"} + + # 链接名称 + name = Column(String(255), nullable=False, index=True, comment="链接名称") + # 链接类型 + type = Column(String(50), default="", comment="链接类型") + # 链接来源 + form = Column(String(50), default="", comment="链接来源") + # 链接图片 + image = Column(String(500), default="", comment="链接图片") + # 链接地址 + url = Column(String(500), default="", comment="链接地址") + # 状态:0-正常 1-停用 + status = Column(Integer, default=0, server_default=text("0"), comment="状态") + # 排序 + sort = Column(Integer, default=0, server_default=text("0"), comment="排序") +``` + +### 2. 表单验证 `src/modules/link/schemas.py` + +```python +# +====================================================================== +# | 模块: 友链请求校验模型层 +# | 说明: 定义友链创建/编辑与状态更新的 Pydantic 校验模型 +# +====================================================================== +"""友链请求校验模型。""" + +from typing import Optional + +from pydantic import BaseModel, Field + +from core.base_schema import BaseSchema + + +class LinkSchema(BaseSchema): + """友链创建/编辑表单""" + # 链接名称 + name: str = Field(..., min_length=1, max_length=150, description="链接名称") + # 链接类型 + type: Optional[str] = Field(default="", max_length=50, description="链接类型") + # 链接来源 + form: Optional[str] = Field(default="", max_length=50, description="链接来源") + # 链接图片 + image: Optional[str] = Field(default="", max_length=500, description="链接图片") + # 链接地址 + url: Optional[str] = Field(default="", max_length=500, description="链接地址") + # 状态 + status: Optional[int] = Field(default=0, ge=0, le=1, description="状态") + # 排序 + sort: Optional[int] = Field(default=0, ge=0, le=99999, description="排序") + + +class LinkStatusSchema(BaseModel): + """友链状态更新表单""" + # 记录ID + id: int = Field(..., gt=0, description="记录ID") + # 状态 + status: int = Field(..., ge=0, le=1, description="状态") +``` + +### 3. 数据访问层 `src/modules/link/repository.py` + +```python +# +====================================================================== +# | 模块: 友链数据访问层 +# | 说明: 定义友链仓储,继承 BaseRepository 获得通用 CRUD 与软删除能力 +# +====================================================================== +"""友链数据访问。""" + +from core.base_repository import BaseRepository +from modules.link.models import Link + + +class LinkRepository(BaseRepository): + """友链数据仓库""" + model = Link +``` + +### 4. 业务逻辑层 `src/modules/link/service.py` + +```python +# +====================================================================== +# | 模块: 友链业务逻辑层 +# | 说明: 定义友链服务,声明差异点(file_fields 自动处理图片), +# | 最后导出服务单例 +# +====================================================================== +"""友链业务逻辑。""" + +from core.base_service import BaseService +from modules.link.models import Link +from modules.link.repository import LinkRepository +from modules.link.schemas import LinkSchema, LinkStatusSchema + + +class LinkService(BaseService): + """友链服务""" + + # ============================================================ + # 基础声明 + # ============================================================ + model = Link + repo_cls = LinkRepository + + # ============================================================ + # 差异点声明 + # ============================================================ + create_schema = LinkSchema + update_schema = LinkSchema + status_schema = LinkStatusSchema + # 分页查询字段与排序 + page_like_fields = ("name",) + page_eq_fields = ("type", "form", "status") + page_order_by = (("sort", "asc"),) + # 枚举显示名映射 + serialize_maps = { + "type": "link_type", + "form": "link_form", + } + # 文件字段:add/update 时自动 save_file,serialize 时自动补全 URL + file_fields = ("image",) + + +# ====================================================================== +# 服务单例 +# ====================================================================== +link_service = LinkService() +``` + +### 5. 请求处理器 `src/modules/link/handlers.py` + +```python +# +====================================================================== +# | 模块: 友链请求处理器层 +# | 说明: 承接友情链接相关的 HTTP 请求,校验权限/演示模式并记录操作日志后, +# | 转交友链服务层处理(分页、详情、增删改、状态、批量删除等) +# +====================================================================== +"""友链请求处理器。""" + +from common.middleware.access_decorators import check_demo, permission_required +from common.middleware.operation_log import operation_log +from core import response as R +from core.base_handler import BaseHandler +from modules.link.service import link_service + + +# ====================================================================== +# 查询友链分页数据 +# ====================================================================== +class LinkPageHandler(BaseHandler): + """查询友链分页数据。""" + + @permission_required("sys:link:list") + async def get(self): + """GET 请求:返回友链分页列表。""" + return await link_service.get_page(self) + + +# ====================================================================== +# 查询友链详情 +# ====================================================================== +class LinkDetailHandler(BaseHandler): + """查询友链详情。""" + + @permission_required("sys:link:detail") + async def get(self, id): + """GET 请求:按 ID 返回友链详情。""" + return R.ok(self, data=link_service.get_detail(id)) + + +# ====================================================================== +# 添加友链 +# ====================================================================== +class LinkAddHandler(BaseHandler): + """添加友链。""" + + @permission_required("sys:link:add") + @check_demo + @operation_log("友链管理", "添加") + async def post(self): + """POST 请求:新增友链。""" + return await link_service.add(self) + + +# ====================================================================== +# 更新友链 +# ====================================================================== +class LinkUpdateHandler(BaseHandler): + """更新友链。""" + + @permission_required("sys:link:update") + @check_demo + @operation_log("友链管理", "更新") + async def put(self): + """PUT 请求:更新友链信息。""" + return await link_service.update(self) + + +# ====================================================================== +# 删除友链 +# ====================================================================== +class LinkDeleteHandler(BaseHandler): + """删除友链。""" + + @permission_required("sys:link:delete") + @check_demo + @operation_log("友链管理", "删除") + async def delete(self, id): + """DELETE 请求:按 ID 删除友链(软删除)。""" + return await link_service.delete(self, id) + + +# ====================================================================== +# 设置友链状态 +# ====================================================================== +class LinkStatusHandler(BaseHandler): + """设置友链状态。""" + + @permission_required("sys:link:status") + @check_demo + @operation_log("友链管理", "设置状态") + async def put(self): + """PUT 请求:修改友链状态。""" + return await link_service.update_status(self) + + +# ====================================================================== +# 批量删除友链 +# ====================================================================== +class LinkBatchDeleteHandler(BaseHandler): + """批量删除友链。""" + + @permission_required("sys:link:delete") + @check_demo + @operation_log("友链管理", "批量删除") + async def post(self): + """POST 请求:批量删除友链(软删除)。""" + return await link_service.batch_delete(self) +``` + +### 6. 路由定义 `src/api/v1/link.py` + +```python +# +====================================================================== +# | 模块: 友链路由层 +# | 说明: 提供友情链接的增删改查、批量删除、状态管理等 API 接口 +# +====================================================================== +"""友链相关路由。""" + +from modules.link.handlers import ( + LinkAddHandler, LinkBatchDeleteHandler, LinkDeleteHandler, + LinkDetailHandler, LinkPageHandler, LinkStatusHandler, + LinkUpdateHandler, +) + +routes = [ + # 查询友链分页数据 + (r"/link/page", LinkPageHandler), + # 查询友链详情 + (r"/link/detail/(\d+)", LinkDetailHandler), + # 添加友链 + (r"/link/add", LinkAddHandler), + # 更新友链 + (r"/link/update", LinkUpdateHandler), + # 删除友链 + (r"/link/delete/(\d+)", LinkDeleteHandler), + # 设置友链状态 + (r"/link/status", LinkStatusHandler), + # 批量删除友链 + (r"/link/batchDelete", LinkBatchDeleteHandler), +] +``` + +### 7. 路由注册 `src/api/v1/router.py` + +```python +# 在 _RESOURCE_MODULES 列表中追加 link 模块 +from api.v1 import link + +_RESOURCE_MODULES = [ + # ... 已有模块 ... + link, +] +``` + +## 前端文件清单 + +| 文件 | 说明 | +|:-----|:-----| +| `ui/src/api/system/link.js` | 接口请求封装 | +| `ui/src/views/system/link/index.vue` | 列表页面 | +| `ui/src/views/system/link/edit.vue` | 编辑弹窗 | +| `ui/src/views/system/link/columns.js` | 表格列定义 | +| `ui/src/views/system/link/querySchemas.js` | 查询条件定义 | + +## 文件关系图 + +``` +src/modules/link/ +├── models.py ← 数据模型(继承 BaseModel) +├── schemas.py ← 请求校验(继承 BaseSchema) +├── repository.py ← 数据访问(继承 BaseRepository) +├── service.py ← 业务逻辑(继承 BaseService) +└── handlers.py ← 请求处理器(继承 BaseHandler) + +src/api/v1/ +├── link.py ← 路由定义(routes 列表) +└── router.py ← 路由注册(_RESOURCE_MODULES) +``` diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume2/example.md b/DjangoAdmin_Tornado_EleVue/zh/volume2/example.md new file mode 100644 index 0000000..44d1f6f --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume2/example.md @@ -0,0 +1,187 @@ +# 示例模块(Example) + +:::tip 说明 +`src/modules/example/` 是一个完整的示例模块,演示了标准 CRUD 模块的六文件结构和开发模式。开发者可以此为模板快速创建新业务模块。 +::: + +## 文件结构 + +``` +src/modules/example/ +├── __init__.py # 模块声明 +├── models.py # 数据模型 +├── schemas.py # 参数验证 +├── repository.py # 数据访问层 +├── service.py # 业务逻辑层 +└── handlers.py # 请求处理器 +``` + +## 各文件说明 + +### models.py — 数据模型 + +```python +from sqlalchemy import Column, String, Integer, text +from core.base_model import BaseModel + +class Example(BaseModel): + """示例模型""" + __tablename__ = "example" + __table_args__ = {"comment": "示例表"} + + name = Column(String(255), nullable=False, comment="名称") + status = Column(Integer, default=0, server_default=text("0"), comment="状态") + sort = Column(Integer, default=0, server_default=text("0"), comment="排序") +``` + +### schemas.py — 参数验证 + +```python +from pydantic import Field +from core.base_schema import BaseSchema, BaseStatusSchema + +class ExampleSchema(BaseSchema): + """示例表单""" + name: str = Field(..., min_length=1, max_length=150, description="名称") + status: int = Field(default=0, ge=0, le=1, description="状态") + sort: int = Field(default=0, ge=0, le=99999, description="排序") + +class ExampleStatusSchema(BaseStatusSchema): + """状态更新表单""" + pass +``` + +### repository.py — 数据访问层 + +```python +from core.base_repository import BaseRepository +from modules.example.models import Example + +class ExampleRepository(BaseRepository): + """示例数据仓库""" + model = Example +``` + +### service.py — 业务逻辑层 + +```python +from core.base_service import BaseService +from modules.example.models import Example +from modules.example.repository import ExampleRepository +from modules.example.schemas import ExampleSchema, ExampleStatusSchema + +class ExampleService(BaseService): + """示例服务""" + model = Example + repo_cls = ExampleRepository + create_schema = ExampleSchema + update_schema = ExampleSchema + status_schema = ExampleStatusSchema + page_like_fields = ("name",) + page_eq_fields = ("status",) + page_order_by = (("sort", "asc"),) + +example_service = ExampleService() +``` + +### handlers.py — 请求处理器 + +```python +from common.middleware.access_decorators import check_demo, permission_required +from common.middleware.operation_log import operation_log +from core import response as R +from core.base_handler import BaseHandler +from modules.example.service import example_service + +class ExamplePageHandler(BaseHandler): + @permission_required("sys:example:list") + async def get(self): + return await example_service.get_page(self) + +class ExampleDetailHandler(BaseHandler): + @permission_required("sys:example:detail") + async def get(self, id): + return R.ok(self, data=example_service.get_detail(id)) + +class ExampleAddHandler(BaseHandler): + @permission_required("sys:example:add") + @check_demo + @operation_log("示例管理", "添加") + async def post(self): + return await example_service.add(self) + +class ExampleUpdateHandler(BaseHandler): + @permission_required("sys:example:update") + @check_demo + @operation_log("示例管理", "更新") + async def put(self): + return await example_service.update(self) + +class ExampleDeleteHandler(BaseHandler): + @permission_required("sys:example:delete") + @check_demo + @operation_log("示例管理", "删除") + async def delete(self, id): + return await example_service.delete(self, id) + +class ExampleStatusHandler(BaseHandler): + @permission_required("sys:example:status") + @check_demo + @operation_log("示例管理", "设置状态") + async def put(self): + return await example_service.update_status(self) + +class ExampleBatchDeleteHandler(BaseHandler): + @permission_required("sys:example:delete") + @check_demo + @operation_log("示例管理", "批量删除") + async def post(self): + return await example_service.batch_delete(self) +``` + +## 路由注册 + +```python +# src/api/v1/example.py +from modules.example.handlers import ( + ExamplePageHandler, ExampleDetailHandler, ExampleAddHandler, + ExampleUpdateHandler, ExampleDeleteHandler, ExampleStatusHandler, + ExampleBatchDeleteHandler, +) + +routes = [ + (r"/example/page", ExamplePageHandler), + (r"/example/detail/(\d+)", ExampleDetailHandler), + (r"/example/add", ExampleAddHandler), + (r"/example/update", ExampleUpdateHandler), + (r"/example/delete/(\d+)", ExampleDeleteHandler), + (r"/example/status", ExampleStatusHandler), + (r"/example/batchDelete", ExampleBatchDeleteHandler), +] +``` + +```python +# src/api/v1/router.py — 在 _RESOURCE_MODULES 中追加 +from api.v1 import example +_RESOURCE_MODULES = [..., example] +``` + +## 开发新模块的步骤 + +基于 Example 模块创建新模块的流程: + +``` +1. 复制 src/modules/example/ 为新目录 +2. 修改 models.py 中的表名和字段 +3. 修改 schemas.py 中的表单验证 +4. 修改 repository.py 中的 model 引用 +5. 修改 service.py 中的配置项 +6. 修改 handlers.py 中的权限字符串和日志描述 +7. 复制 src/api/v1/example.py 为新路由文件 +8. 在 router.py 中注册新路由 +9. 在菜单管理中配置权限节点 +``` + +## 总结 + +Example 模块是标准 CRUD 模块的参考实现,包含 models → schemas → repository → service → handlers → 路由六层结构。开发新模块时以此为模板,修改差异点即可快速完成。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume2/faq.md b/DjangoAdmin_Tornado_EleVue/zh/volume2/faq.md new file mode 100644 index 0000000..e9c1218 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume2/faq.md @@ -0,0 +1,169 @@ +# 常见问题与排错 + +本章汇总模块开发过程中的常见问题和解决方案。 + +## 后端问题 + +### Q1: 启动报错 `Table 'xxx.tornado_position' doesn't exist` + +**原因**:模型定义后未执行建表。 + +**解决**: + +```bash +# 方式一:脚本自动建表 +python scripts/init_db.py + +# 方式二:Alembic 迁移 +alembic revision --autogenerate -m "add position table" +alembic upgrade head + +# 方式三:手动建表(参考 model.md 中的 DDL) +``` + +### Q2: 接口返回 `{"code": 1, "msg": "权限不足"}` + +**原因**:权限节点未配置或未分配给当前用户角色。 + +**排查步骤**: +1. 确认菜单管理中已添加权限节点,权限标识与代码中 `@permission_required` 完全一致 +2. 确认权限节点已分配给当前用户的角色 +3. 使用 admin 账号(ID=1)测试,admin 自动跳过权限校验 + +### Q3: 唯一性校验不生效 + +**原因**:`unique_fields` 配置错误。 + +**排查步骤**: +1. 确认 key 是模型字段名(如 `name`),不是中文名 +2. 确认 Service 类中声明了 `unique_fields` +3. 确认调用的是 `service.add()` 或 `service.update()`,而非直接操作 repo + +### Q4: 分页查询返回空数据 + +**原因**:分页参数名不匹配或字段配置错误。 + +**排查步骤**: +1. 前端传参名应为 `pageNo` 和 `pageSize`(驼峰) +2. 确认 `page_like_fields` 和 `page_eq_fields` 中的字段名与模型一致 +3. 检查数据库中是否有数据(`is_delete=0` 的记录) + +### Q5: 删除时报错 "存在引用" + +**原因**:`_before_delete` 钩子检测到关联数据。 + +**解决**:先调整引用该记录的其他数据,再执行删除。如岗位被用户引用时,需先修改用户的岗位字段。 + +### Q6: `batch_delete` 端点报 500 错误 + +**原因**:Handler 方法未正确继承 `BaseHandler`。 + +**解决**:批量删除 Handler 必须继承 `BaseHandler` 并使用 `async def`: + +```python +class PositionBatchDeleteHandler(BaseHandler): + @permission_required("sys:position:delete") + @check_demo + async def post(self): + return await position_service.batch_delete(self) +``` + +## 前端问题 + +### Q7: 页面白屏,控制台报 `Failed to fetch dynamically imported module` + +**原因**:路由组件路径配置错误。 + +**排查步骤**: +1. 确认路由中的 `component` 路径与实际文件路径一致 +2. 确认后端菜单的 `component` 字段值正确(如 `system/position/index`) +3. 检查文件名大小写是否匹配 + +### Q8: 搜索功能不生效 + +**原因**:搜索表单的 `submit` 事件未正确绑定。 + +**排查步骤**: +1. 确认 `BasicForm` 绑定了 `@submit="handleSearch"` 事件 +2. 确认 `handleSearch` 中调用了 `reload({ searchInfo: values })` +3. 确认后端 `page_like_fields` 和 `page_eq_fields` 配置了对应字段 + +### Q9: 编辑弹窗不回填数据 + +**原因**:`openModal` 未传入记录数据。 + +**解决**: + +```javascript +// 正确:第一个参数 true 表示编辑模式,第二个参数是记录数据 +openModal(true, record); +``` + +### Q10: 操作按钮不显示 + +**原因**:`v-perm` 权限标识与后端不一致。 + +**排查步骤**: +1. 确认 `v-perm` 中的权限字符串与后端 `@permission_required` 完全一致 +2. 确认当前用户拥有对应权限 +3. 检查 `TableAction` 中的 `auth` 字段是否正确 + +## 通用问题 + +### Q11: 接口返回 401 Unauthorized + +**原因**:JWT Token 过期或未携带。 + +**解决**: +1. 确认请求头包含 `Authorization: Bearer ` +2. Token 过期后需调用刷新接口或重新登录 +3. 前端 `defHttp` 自动处理 Token 注入,检查是否正确引入 + +### Q12: 接口返回 403 Forbidden(演示模式) + +**原因**:演示模式下写操作被拦截。 + +**解决**:将 `.env` 中的 `TORNADO_DEMO` 设为 `False`,或移除端点上的 `@check_demo` 装饰器(仅开发阶段)。 + +### Q13: 数据库字段中文乱码 + +**原因**:数据库或表的字符集不是 UTF-8。 + +**解决**: +- MySQL:确保数据库和表使用 `utf8mb4` 字符集 +- PostgreSQL:建库时指定 `ENCODING 'UTF8'` + +## 开发调试技巧 + +### 查看 SQL 日志 + +在 `.env` 中开启 SQL 调试: + +```bash +DB_DEBUG=True +``` + +重启后所有 SQL 查询都会输出到控制台。 + +### 使用 curl 调试 + +使用 curl 或 Postman 等工具直接测试接口: + +```bash +# 登录获取 Token +curl -X POST http://127.0.0.1:8041/api/v1/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"123456","code":"xxxx","key":"xxxx"}' + +# 携带 Token 调用业务接口 +curl http://127.0.0.1:8041/api/v1/position/page \ + -H "Authorization: Bearer " +``` + +### 检查权限配置 + +如果不确定权限是否配置正确,可以用 admin 账号测试。admin(ID=1)自动跳过所有权限校验。 + +## 总结 + +模块开发常见问题主要集中在:字段类型不匹配、唯一性校验遗漏、软删除过滤缺失、分页参数命名不一致、权限节点未配置等。遇到问题时优先检查后端日志,确认接口入参和返回值是否符合预期。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume2/fe-api.md b/DjangoAdmin_Tornado_EleVue/zh/volume2/fe-api.md new file mode 100644 index 0000000..797cc1c --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume2/fe-api.md @@ -0,0 +1,185 @@ +# 前端 API 层 + +前端 API 层负责封装后端接口的 HTTP 请求。每个业务模块对应一个 API 文件,使用项目封装的 Axios 工具发送请求。 + +## 文件位置 + +``` +ui/src/api/system/position.ts +``` + +## 完整代码 + +```typescript +import { http } from '@/utils/http/axios'; + +/** + * @description: 岗位列表(分页) + */ +export function getPositionList(params?) { + return http.request({ + url: '/position/page', + method: 'GET', + params, + }); +} + +/** + * 获取全部岗位列表(无分页,用于下拉选择) + * @param params 参数 + * @returns 返回结果 + */ +export function getPositionAllList(params?) { + return http.request({ + url: '/position/list', + method: 'GET', + params, + }); +} + +/** + * @description: 根据ID获取详情 + */ +export function getPositionDetail(positionId) { + return http.request({ + url: '/position/detail/' + positionId, + method: 'get', + }); +} + +/** + * @description: 添加岗位 + */ +export function positionAdd(data: any) { + return http.request({ + url: '/position/add', + method: 'POST', + data, + }); +} + +/** + * @description: 更新岗位 + */ +export function positionUpdate(data: any) { + return http.request({ + url: '/position/update', + method: 'PUT', + data, + }); +} + +/** + * @description: 删除岗位 + */ +export function positionDelete(positionId) { + return http.request({ + url: '/position/delete/' + positionId, + method: 'DELETE', + }); +} + +/** + * @description: 批量删除岗位 + */ +export function positionBatchDelete(data: any) { + return http.request({ + url: '/position/batchDelete', + method: 'DELETE', + data, + }); +} +``` + +## 代码解析 + +### HTTP 客户端 + +```typescript +import { http } from '@/utils/http/axios'; +``` + +`http` 是项目封装的 Axios 实例,自动处理: +- 请求拦截:添加 JWT Token(`Authorization: Bearer xxx`) +- 响应拦截:统一处理错误码、弹出提示 +- 基础路径:自动拼接 `/api/v1` 前缀 + +### 请求方式 + +```typescript +http.request({ + url: '/position/page', // 接口路径(不含 /api/v1 前缀) + method: 'GET', // HTTP 方法 + params, // GET 参数(拼接到 URL 查询字符串) +}) +``` + +| 参数 | 用途 | +|:----|:----| +| `url` | 接口路径,与后端 router 定义的 path 对应 | +| `method` | HTTP 方法:GET / POST / PUT / DELETE | +| `params` | GET 请求参数(拼接到 URL) | +| `data` | POST/PUT/DELETE 请求体(JSON) | + +### 函数命名规范 + +| 模式 | 示例 | 说明 | +|:----|:----|:----| +| `get{Module}List` | `getPositionList` | 分页列表查询 | +| `get{Module}AllList` | `getPositionAllList` | 全量列表查询 | +| `get{Module}Detail` | `getPositionDetail` | 详情查询 | +| `{module}Add` | `positionAdd` | 新增 | +| `{module}Update` | `positionUpdate` | 更新 | +| `{module}Delete` | `positionDelete` | 单条删除 | +| `{module}BatchDelete` | `positionBatchDelete` | 批量删除 | + +### 参数传递区别 + +```typescript +// GET:参数通过 params 传递(拼接到 URL 查询字符串) +http.request({ url: '/position/page', method: 'GET', params }) + +// POST/PUT/DELETE:参数通过 data 传递(放入请求体 JSON) +http.request({ url: '/position/add', method: 'POST', data }) + +// 路径参数:直接拼接到 URL +http.request({ url: '/position/delete/' + positionId, method: 'DELETE' }) +``` + +## 页面中的调用方式 + +```vue + +``` + +## 开发要点 + +1. **一个模块一个文件**:放在 `ui/src/api/{group}/` 目录下 +2. **URL 不含 `/api/v1` 前缀**:`http` 自动拼接 +3. **GET 用 `params`,POST/PUT/DELETE 用 `data`**:参数传递方式不同 +4. **路径参数直接拼接到 URL**:如 `/position/delete/` + id +5. **批量删除的参数是数组**:`data: [1, 2, 3]` + +## 总结 + +前端 API 层使用封装后的 `VAxios` 发送请求,URL 使用相对路径(不含 `/api/v1` 前缀),函数命名采用 `名词+动词` 格式。GET 请求用 `params`,POST/PUT/DELETE 用 `data`,响应拦截器自动处理错误。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume2/fe-router.md b/DjangoAdmin_Tornado_EleVue/zh/volume2/fe-router.md new file mode 100644 index 0000000..0f79bbe --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume2/fe-router.md @@ -0,0 +1,139 @@ +# 前端路由与菜单 + +前端路由定义页面的访问路径,菜单是路由的可视化展示。项目的路由和菜单由后端动态返回,前端只需注册路由组件映射。 + +## 路由配置 + +### 文件位置 + +路由配置文件按模块分组: + +``` +ui/src/router/routes/system.ts +``` + +### 添加路由 + +在 `system.ts` 中添加岗位管理的路由: + +```typescript +{ + path: '/system', + name: 'System', + component: Layout, + meta: { title: '系统管理', icon: 'setting' }, + children: [ + { + path: 'position', + name: 'Position', + component: () => import('/@/views/system/position/index.vue'), + meta: { title: '岗位管理' }, + }, + // ... 其他系统管理路由 + ], +} +``` + +### 路由字段说明 + +| 字段 | 说明 | +|:----|:----| +| `path` | 路由路径(子路由不带 `/`) | +| `name` | 路由名称(唯一标识) | +| `component` | 组件引用(懒加载) | +| `meta.title` | 菜单显示标题 | +| `meta.icon` | 菜单图标 | + +### 路由懒加载 + +```typescript +component: () => import('/@/views/system/position/index.vue') +``` + +使用动态 `import()` 实现路由懒加载,访问时才加载组件代码,优化首屏加载速度。 + +## 后端菜单配置 + +路由的实际注册由后端菜单数据驱动。在「系统管理 → 菜单管理」中添加菜单节点: + +| 字段 | 值 | +|:----|:----| +| 菜单名称 | 岗位管理 | +| 菜单类型 | 菜单(type=1) | +| 路由路径 | `/system/position` | +| 组件路径 | `system/position/index` | +| 菜单图标 | 选择合适图标 | +| 排序号 | 设置显示顺序 | +| 状态 | 启用 | + +### 菜单数据返回格式 + +后端接口 `/api/v1/menu/list` 返回的菜单数据: + +```json +{ + "code": 0, + "data": [ + { + "id": 10, + "name": "系统管理", + "parent_id": 0, + "path": "/system", + "component": "Layout", + "type": 1, + "icon": "setting", + "children": [ + { + "id": 101, + "name": "岗位管理", + "parent_id": 10, + "path": "/system/position", + "component": "system/position/index", + "type": 1, + "icon": "post" + } + ] + } + ] +} +``` + +### 前端路由生成 + +前端在登录后请求菜单数据,动态生成路由: + +``` +登录成功 → 请求 /api/v1/menu/list → 生成动态路由 → 注册到 Vue Router → 渲染菜单 +``` + +## 菜单与权限的关系 + +``` +菜单管理 +├── 系统管理(目录) +│ ├── 岗位管理(菜单) +│ │ ├── 岗位查询(权限)→ sys:position:page +│ │ ├── 岗位新增(权限)→ sys:position:add +│ │ ├── 岗位编辑(权限)→ sys:position:update +│ │ └── 岗位删除(权限)→ sys:position:delete +│ └── ... +└── ... +``` + +| 节点类型 | type | 作用 | +|:----|:----|:----| +| 目录 | 0 | 菜单分组,不对应页面 | +| 菜单 | 1 | 对应一个页面路由 | +| 权限 | 2 | 对应一个操作权限,不在菜单中显示 | + +## 开发要点 + +1. **路由文件按模块分组**:`system.ts`、`content.ts` 等 +2. **组件使用懒加载**:`() => import(...)` +3. **菜单数据由后端驱动**:前端只需注册组件映射 +4. **组件路径与后端 `component` 字段对应**:如 `system/position/index` +5. **新增模块只需在对应分组路由文件中追加子路由** + +## 总结 + +前端路由使用 Vue Router 4,菜单路由由后端动态返回,前端通过 `addRoute` 动态注册。路由守卫负责权限校验和登录状态检查,未登录自动跳转登录页。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume2/fe-view.md b/DjangoAdmin_Tornado_EleVue/zh/volume2/fe-view.md new file mode 100644 index 0000000..3639881 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume2/fe-view.md @@ -0,0 +1,512 @@ +# 前端页面视图 + +前端页面视图是用户直接交互的界面。项目使用 Vue3 + ElementPlus + Vite 构建。一个完整的 CRUD 页面由 4 个文件组成: + +``` +ui/src/views/system/position/ +├── index.vue # 主页面:搜索 + 表格 + 操作 +├── edit.vue # 编辑弹窗:新增/编辑表单 +├── columns.ts # 表格列定义 +└── querySchemas.ts # 搜索表单 Schema +``` + +## 1. 主页面 `index.vue` + +主页面是模块的入口页面,包含搜索表单、数据表格和操作按钮。 + +### 完整代码 + +```vue + + + +``` + +### 代码解析 + +**页面结构**:`PageWrapper` 包裹两个 `el-card`,上方是搜索表单,下方是数据表格。 + +**搜索表单**:通过 `useForm` 注册,Schema 从 `querySchemas.ts` 导入。`submitOnReset: true` 表示重置时自动触发查询。 + +**数据表格**:通过 `BasicTable` 组件渲染,`request` 属性绑定数据加载函数,`columns` 从 `columns.ts` 导入。 + +**操作栏**:通过 `actionColumn` 的 `render` 函数使用 `h()` 渲染 `TableAction`,支持权限控制(`auth`)。 + +**编辑弹窗**:使用 `defineAsyncComponent` 懒加载 `edit.vue`,通过 `v-model:visible` 控制显示,`positionId` 传入编辑记录 ID(0 表示新增)。 + +**删除逻辑**:单条删除传入 `record`,批量删除从 `selectionData` 取已选 ID。共用同一个 `handleDelete` 方法。 + +## 2. 编辑弹窗 `edit.vue` + +编辑弹窗是独立组件,负责新增和编辑操作的表单展示与提交。 + +### 完整代码 + +```vue + + + +``` + +### 代码解析 + +**Props 接收**: +- `visible`:控制弹窗显示/隐藏,通过 `v-model:visible` 双向绑定 +- `positionId`:编辑记录的 ID,为 0 时表示新增模式 + +**表单数据**:使用 `reactive` 定义,字段与后端 Schema 对应。 + +**新增/编辑判断**:`props.positionId ? positionUpdate : positionAdd`,通过 ID 是否为 0 区分。 + +**防重复提交**:`useLockFn` 包装提交函数,点击后按钮变为 loading 状态,防止重复提交。 + +**数据回填**:`onMounted` 时判断如果是编辑模式,调用 `getPositionDetail` 获取数据并填充表单。 + +**事件通信**: +- `emit('update:visible', false)`:关闭弹窗 +- `emit('success')`:通知父页面刷新表格 + +## 3. 表格列定义 `columns.ts` + +定义表格的列配置,包括列名、字段名、宽度和自定义渲染。 + +### 完整代码 + +```typescript +import { h } from 'vue'; +import { ElTag } from 'element-plus'; + +export const columns = [ + { + type: 'selection', + }, + { + label: 'ID', + prop: 'id', + fixed: 'left', + width: 50, + }, + { + label: '岗位名称', + prop: 'name', + minWidth: 100, + }, + { + label: '岗位状态', + prop: 'status', + minWidth: 100, + render(record) { + return h( + ElTag, + { + type: record.row.status == 1 ? 'success' : 'danger', + }, + { + default: () => (record.row.status == 1 ? '正常' : '停用'), + }, + ); + }, + }, + { + label: '排序', + prop: 'sort', + minWidth: 100, + }, + { + label: '创建人', + prop: 'createUser', + minWidth: 100, + }, + { + label: '创建时间', + prop: 'createTime', + width: 180, + }, +]; +``` + +### 代码解析 + +**选择列**:`type: 'selection'` 自动添加复选框列,配合 `@selection-change` 事件实现批量选择。 + +**固定列**:`fixed: 'left'` 固定在左侧,`fixed: 'right'` 固定在右侧(操作列)。 + +**自定义渲染**:`render(record)` 使用 Vue 的 `h()` 函数创建虚拟 DOM。状态列将 `1/2` 渲染为绿色/红色标签。 + +**宽度设置**: +- `width`:固定宽度 +- `minWidth`:最小宽度,可自适应拉伸 + +## 4. 搜索表单 `querySchemas.ts` + +定义搜索表单的字段配置,使用项目封装的 `FormSchema` 类型。 + +### 完整代码 + +```typescript +import { FormSchema } from '@/components/Form/index'; +export const schemas: FormSchema[] = [ + { + field: 'name', + component: 'Input', + label: '岗位名称', + componentProps: { + placeholder: '请输入岗位名称', + }, + }, + { + field: 'status', + component: 'Select', + label: '状态', + componentProps: { + placeholder: '请选择状态', + clearable: true, + options: [ + { + label: '正常', + value: '1', + }, + { + label: '禁用', + value: '2', + }, + ], + }, + }, +]; +``` + +### 代码解析 + +**FormSchema 字段**: + +| 字段 | 说明 | +|:----|:----| +| `field` | 字段名,与后端查询参数对应 | +| `component` | 表单组件类型:`Input` / `Select` / `DatePicker` 等 | +| `label` | 标签文本 | +| `componentProps` | 组件属性,透传给 ElementPlus 组件 | + +**Select 组件**:`options` 定义下拉选项,`clearable: true` 允许清空选择。 + +## 文件关系图 + +``` +index.vue(主页面) + ├── import schemas from './querySchemas' ← 搜索表单配置 + ├── import columns from './columns' ← 表格列配置 + ├── import editDialog from './edit.vue' ← 编辑弹窗组件 + └── import API from '@/api/system/position' ← 接口请求 + +edit.vue(编辑弹窗) + └── import API from '@/api/system/position' ← 接口请求 +``` + +## 开发要点 + +1. **一个模块一个目录**:放在 `ui/src/views/{group}/{module}/` 下 +2. **4 个文件各司其职**:主页面、编辑弹窗、列定义、搜索 Schema +3. **编辑弹窗是独立组件**:通过 Props 接收 ID,通过 Emit 通知父页面 +4. **列定义使用 `h()` 渲染复杂内容**:如状态标签、操作按钮 +5. **搜索 Schema 使用 `FormSchema` 类型**:配置化定义表单字段 +6. **新增/编辑通过 `positionId` 区分**:0 为新增,非 0 为编辑 +7. **防重复提交使用 `useLockFn`**:提交时按钮 loading + +## 总结 + +前端页面统一使用 `PageWrapper` + `BasicForm` + `BasicTable` + `TableAction` 四件套,分页由 `BasicTable` 内置处理。每个模块包含 `index.vue`(列表页)、`edit.vue`(编辑弹窗)、`columns.ts`(列定义)、`querySchemas.ts`(搜索配置)四个文件。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume2/handler.md b/DjangoAdmin_Tornado_EleVue/zh/volume2/handler.md new file mode 100644 index 0000000..9e36ff4 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume2/handler.md @@ -0,0 +1,157 @@ +# 请求处理器(Handler) + +Handler 层是 HTTP 接口层,负责接收请求、校验权限、记录操作日志,然后委托给 Service 层处理。每个业务模块对应一个 `handlers.py` 文件,所有 Handler 继承 `BaseHandler`。 + +## 文件位置 + +``` +src/modules/link/handlers.py +``` + +## 完整代码 + +```python +from common.middleware.access_decorators import check_demo, permission_required +from common.middleware.operation_log import operation_log +from core import response as R +from core.base_handler import BaseHandler +from modules.link.service import link_service + + +class LinkPageHandler(BaseHandler): + """查询友链分页数据。""" + + @permission_required("sys:link:list") + async def get(self): + return await link_service.get_page(self) + + +class LinkDetailHandler(BaseHandler): + """查询友链详情。""" + + @permission_required("sys:link:detail") + async def get(self, id): + return R.ok(self, data=link_service.get_detail(id)) + + +class LinkAddHandler(BaseHandler): + """添加友链。""" + + @permission_required("sys:link:add") + @check_demo + @operation_log("友链管理", "添加") + async def post(self): + return await link_service.add(self) + + +class LinkUpdateHandler(BaseHandler): + """更新友链。""" + + @permission_required("sys:link:update") + @check_demo + @operation_log("友链管理", "更新") + async def put(self): + return await link_service.update(self) + + +class LinkDeleteHandler(BaseHandler): + """删除友链。""" + + @permission_required("sys:link:delete") + @check_demo + @operation_log("友链管理", "删除") + async def delete(self, id): + return await link_service.delete(self, id) + + +class LinkStatusHandler(BaseHandler): + """设置友链状态。""" + + @permission_required("sys:link:status") + @check_demo + @operation_log("友链管理", "设置状态") + async def put(self): + return await link_service.update_status(self) + + +class LinkBatchDeleteHandler(BaseHandler): + """批量删除友链。""" + + @permission_required("sys:link:delete") + @check_demo + @operation_log("友链管理", "批量删除") + async def post(self): + return await link_service.batch_delete(self) +``` + +## 代码解析 + +### Handler 类 + +```python +class LinkPageHandler(BaseHandler): +``` + +每个 Handler 继承 `BaseHandler`(即 `tornado.web.RequestHandler`),负责一个 HTTP 端点。与 Tornado 的 Endpoint 函数不同,Tornado 使用类来组织处理器。 + +### 装饰器说明 + +| 装饰器 | 作用 | 位置 | +|:----|:----|:----| +| `@permission_required("sys:link:list")` | RBAC 权限校验 | 类方法上 | +| `@check_demo` | 演示模式拦截(仅写操作) | 权限装饰器之后 | +| `@operation_log("模块", "动作")` | 操作日志记录 | 最内层 | + +**装饰器顺序必须严格遵守**:权限 → 演示模式 → 操作日志。 + +### 路径参数 + +```python +async def get(self, id): +``` + +Tornado 的路径参数通过正则捕获组传入方法参数。路由定义 `(r"/link/detail/(\d+)", LinkDetailHandler)` 中的 `(\d+)` 对应 `id` 参数。 + +### 响应封装 + +- **Service 返回 R 对象的**(如 `add`、`update`、`delete`):直接 `return` +- **Service 返回原始数据的**(如 `get_detail`):用 `R.ok(self, data=...)` 包装 + +### HTTP 方法映射 + +| 操作 | HTTP 方法 | Handler 方法 | +|:----|:----|:----| +| 分页查询 | GET | `async def get(self)` | +| 详情查询 | GET | `async def get(self, id)` | +| 新增 | POST | `async def post(self)` | +| 更新 | PUT | `async def put(self)` | +| 删除 | DELETE | `async def delete(self, id)` | +| 状态变更 | PUT | `async def put(self)` | +| 批量删除 | POST | `async def post(self)` | + +### 权限字符串命名规范 + +``` +sys:{module}:{action} +``` + +| 动作 | HTTP 方法 | 说明 | +|:----|:----|:----| +| `list` | GET | 分页/列表查询 | +| `detail` | GET | 详情查询 | +| `add` | POST | 新增 | +| `update` | PUT | 更新 | +| `delete` | DELETE | 单条/批量删除 | +| `status` | PUT | 状态变更 | + +## 开发要点 + +1. **Handler 层不含业务逻辑**:仅做权限校验、日志记录和请求分发 +2. **装饰器顺序**:权限 → 演示模式 → 操作日志 +3. **写操作必须加 `@check_demo`**:防止演示环境被修改 +4. **写操作建议加 `@operation_log`**:记录操作日志用于审计 +5. **所有方法都是 `async def`**:Tornado Handler 方法默认异步 + +## 总结 + +Handler 层是 HTTP 请求的入口,负责权限校验、操作日志记录和请求分发,不含业务逻辑。装饰器顺序固定为权限 → 演示模式 → 操作日志,写操作必须加 `@check_demo`,所有逻辑委托 Service 处理。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume2/model.md b/DjangoAdmin_Tornado_EleVue/zh/volume2/model.md new file mode 100644 index 0000000..ab98701 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume2/model.md @@ -0,0 +1,120 @@ +# 数据模型(Model) + +数据模型是模块开发的第一步,用于定义数据库表结构。项目使用 `SQLAlchemy ORM`,模型类通过继承 `BaseModel` 自动获得通用字段和方法。 + +## 文件位置 + +``` +src/modules/link/models.py +``` + +## 完整代码 + +```python +# +====================================================================== +# | 模块: 友链数据模型层 +# | 说明: 定义友链表结构,继承 BaseModel 获得通用字段与序列化能力 +# +====================================================================== +"""友链数据模型。""" + +from sqlalchemy import Column, Integer, String, text + +from config.database import DB_PREFIX +from core.base_model import BaseModel + + +class Link(BaseModel): + """友情链接模型""" + __tablename__ = DB_PREFIX + "link" + __table_args__ = {"comment": "友情链接表"} + + # 链接名称 + name = Column(String(255), nullable=False, index=True, comment="链接名称") + # 链接类型 + type = Column(String(50), default="", comment="链接类型") + # 链接来源 + form = Column(String(50), default="", comment="链接来源") + # 链接图片 + image = Column(String(500), default="", comment="链接图片") + # 链接地址 + url = Column(String(500), default="", comment="链接地址") + # 状态:0-正常 1-停用 + status = Column(Integer, default=0, server_default=text("0"), comment="状态") + # 排序 + sort = Column(Integer, default=0, server_default=text("0"), comment="排序") +``` + +## 代码解析 + +### 基类继承 + +```python +class Link(BaseModel): +``` + +| 基类 | 来源 | 提供能力 | +|:----|:----|:----| +| `BaseModel` | `core/base_model.py` | 通用字段:id、create_user、create_time、update_user、update_time、is_delete;`to_dict()` 序列化方法(自动排除 password/salt/is_delete) | + +:::tip BaseModel 说明 +`BaseModel` 是所有业务模型的基类,提供6个通用审计字段和 `to_dict()` 序列化方法。模型实例不暴露 `save()/delete()` 方法,数据操作通过 Repository 层进行。 +::: + +### 表名配置 + +```python +__tablename__ = DB_PREFIX + "link" +``` + +`DB_PREFIX` 从 `.env` 读取(默认 `tornado_`),最终表名为 `tornado_link`。所有业务表都使用此前缀,便于区分不同应用的同库表。 + +### 表注释 + +```python +__table_args__ = {"comment": "友情链接表"} +``` + +使用 `__table_args` 字典设置表注释,同步到数据库元数据。 + +### 字段定义 + +```python +name = Column(String(255), nullable=False, index=True, comment="链接名称") +``` + +| 参数 | 说明 | +|:----|:----| +| `String(255)` | 字段类型,VARCHAR(255) | +| `nullable=False` | 非空约束 | +| `index=True` | 创建索引,加速查询 | +| `comment` | 字段注释,同步到数据库 | + +```python +status = Column(Integer, default=0, server_default=text("0"), comment="状态") +``` + +`default=0` 是 Python 端默认值,`server_default=text("0")` 是数据库端默认值。两者都设置可确保无论通过 ORM 还是直接 SQL 插入,都能正确赋默认值。 + +### 枚举值约定 + +状态字段使用 `0` 和 `1` 而非 `1` 和 `2`: + +| 字段 | 值 | 含义 | +|:----|:----|:----| +| `status` | `0` | 正常 | +| `status` | `1` | 停用 | +| `is_delete` | `0` | 未删除 | +| `is_delete` | `1` | 已删除 | + +## 开发要点 + +1. **所有模型继承 `BaseModel`**:自动获得通用字段和序列化能力 +2. **表名必须加 `DB_PREFIX` 前缀**:`DB_PREFIX + "表名"` +3. **使用 `__table_args__` 设置表注释**:字典格式 `{"comment": "表注释"}` +4. **查询频繁的字段设置索引**:`index=True` +5. **同时设置 `default` 和 `server_default`**:ORM 和直 SQL 都能正确赋默认值 +6. **模型不暴露 save/delete 方法**:数据操作通过 Repository 层进行 + +## 总结 + +数据模型通过继承 `BaseModel` 获得通用审计字段和序列化能力。表名使用 `DB_PREFIX` 前缀,字段定义同时设置 Python 端和数据库端默认值。模型只定义表结构,不包含业务逻辑。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume2/overview.md b/DjangoAdmin_Tornado_EleVue/zh/volume2/overview.md new file mode 100644 index 0000000..369c0a7 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume2/overview.md @@ -0,0 +1,97 @@ +# 概述与目标 + +本章以「岗位管理(Position)」模块为完整案例,手把手演示从零开发一个业务模块的全流程。学完本章后,你将掌握项目中任意模块的开发方式。 + +## 你将学到什么 + +通过本章的实战演练,你将完整经历以下开发环节: + +| 环节 | 涉及文件 | 说明 | +|:----|:----|:----| +| 数据模型 | `models.py` | 定义数据库表结构 | +| 请求校验 | `schemas.py` | Pydantic 表单验证 | +| 数据访问 | `repository.py` | 封装数据库查询 | +| 业务逻辑 | `service.py` | 实现增删改查与业务规则 | +| API 端点 | `handlers.py` | 定义 HTTP 接口 | +| 路由注册 | `router.py` | 挂载路由到应用 | +| 权限配置 | 菜单管理 | 配置权限节点 | +| 前端对接 | API + 页面 + 路由 | 前端完整开发 | + +## 案例模块:岗位管理 + +选择「岗位管理」作为案例的原因: + +1. **结构典型**:包含完整的 CRUD + 状态管理 + 删除校验,覆盖大多数业务场景 +2. **复杂度适中**:字段少(name / status / sort),逻辑清晰,适合入门 +3. **依赖简单**:仅被 User 模块引用,无复杂关联关系 +4. **代码精简**:后端 4 个文件 + 前端 3 个文件,总计约 500 行代码 + +## 模块文件总览 + +``` +后端(src/) +├── modules/position/ +│ ├── __init__.py # 模块初始化(导出 routes) +│ ├── models.py # 数据模型 +│ ├── schemas.py # 表单验证 +│ ├── repository.py # 数据访问层 +│ ├── service.py # 业务逻辑层 +│ └── handlers.py # 请求处理器层 +└── api/v1/ + └── position.py # 路由定义(导出 routes 列表) + +前端(ui/src/) +├── api/system/ +│ └── position.ts # API 请求封装 +├── views/system/ +│ └── position/ +│ ├── index.vue # 主页面:搜索 + 表格 + 操作 +│ ├── edit.vue # 编辑弹窗:新增/编辑表单 +│ ├── columns.ts # 表格列定义 +│ └── querySchemas.ts # 搜索表单 Schema +└── router/routes/ + └── system.ts # 路由配置(追加) +``` + +## 数据库表结构 + +岗位表 `tornado_position` 结构如下: + +| 字段 | 类型 | 说明 | +|:----|:----|:----| +| id | int | 主键,自增 | +| name | varchar(255) | 岗位名称 | +| status | int | 状态:1-在用 2-停用 | +| sort | int | 排序号 | +| create_user | varchar(50) | 创建人 | +| create_time | datetime | 创建时间 | +| update_user | varchar(50) | 更新人 | +| update_time | datetime | 更新时间 | +| is_delete | int | 软删除标记:0-正常 1-已删除 | + +:::tip 温馨提示 +其中 `id`、`create_user`、`create_time`、`update_user`、`update_time`、`is_delete` 由基类 `BaseModel` 自动提供,业务字段仅需定义 `name`、`status`、`sort`。 +::: + +## 开发顺序 + +建议按以下顺序逐步开发,每完成一步即可验证: + +``` +1. models.py → 建表,确认数据库中有 tornado_position 表 +2. schemas.py → 定义验证规则(无独立验证方式,后续端点调用时自动校验) +3. repository.py → 数据访问层(通常只需声明类,无需写代码) +4. service.py → 业务逻辑层(声明差异点 + 自定义方法) +5. handlers.py → 请求处理器(声明路由 + 权限装饰器) +6. api/v1/position.py → 注册路由(导出 routes 列表,在 router.py 中自动聚合) +7. 权限配置 → 在菜单管理中添加权限节点 +8. 前端对接 → API → 页面 → 路由 +``` + +:::tip 温馨提示 +本章每一步都附有完整代码和详细注释。建议对照项目中 `src/modules/position/` 目录下的实际源码一起阅读,加深理解。 +::: + +## 总结 + +模块开发实战以岗位管理(position)为例,完整演示从 Model → Schema → Repository → Service → Handler → 路由注册 → 权限配置 → 前端 API → 前端页面的全流程。每一步都附有完整代码和详细注释,建议对照实际源码一起阅读。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume2/permission.md b/DjangoAdmin_Tornado_EleVue/zh/volume2/permission.md new file mode 100644 index 0000000..b8cbdf2 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume2/permission.md @@ -0,0 +1,105 @@ +# 权限节点配置 + +后端代码中的 `@permission_required("sys:position:page")` 装饰器定义了接口所需的权限字符串,但权限节点本身需要在数据库中配置,才能在「菜单管理」中分配给角色。 + +## 权限字符串格式 + +``` +sys:{module}:{action} +``` + +| 部分 | 说明 | 示例 | +|:----|:----|:----| +| `sys` | 系统前缀(固定) | `sys` | +| `module` | 模块名(小写) | `position` | +| `action` | 操作名 | `page` / `add` / `update` / `delete` | + +## Position 模块的权限节点 + +| 权限字符串 | 说明 | HTTP 方法 | 路径 | +|:----|:----|:----|:----| +| `sys:position:page` | 分页查询 | GET | `/api/v1/position/page` | +| `sys:position:detail` | 详情查询 | GET | `/api/v1/position/detail/{id}` | +| `sys:position:list` | 列表查询 | GET | `/api/v1/position/getPositionList` | +| `sys:position:add` | 新增岗位 | POST | `/api/v1/position/add` | +| `sys:position:update` | 更新岗位 | PUT | `/api/v1/position/update` | +| `sys:position:delete` | 删除岗位 | DELETE | `/api/v1/position/delete/{id}` | +| `sys:position:batchDelete` | 批量删除 | POST | `/api/v1/position/batchDelete` | +| `sys:position:status` | 状态变更 | PUT | `/api/v1/position/status` | + +## 配置步骤 + +### 第一步:添加菜单节点 + +登录管理后台,进入「系统管理 → 菜单管理」: + +1. 找到或创建「岗位管理」菜单项 +2. 在该菜单下添加「权限」类型子节点 +3. 填写以下信息: + +| 字段 | 值 | +|:----|:----| +| 菜单类型 | 权限 | +| 权限标识 | `sys:position:page` | +| 菜单名称 | 岗位查询 | + +### 第二步:批量添加权限节点 + +为每个接口重复上述步骤,添加全部 8 个权限节点。 + +:::tip 温馨提示 +实际开发中,查询类权限(page、detail、list)通常合并为一个节点 `sys:position:list`,减少配置工作量。写操作(add、update、delete、batchDelete、status)建议独立配置,便于细粒度控制。 +::: + +### 第三步:分配给角色 + +权限节点创建并启用后,需将其分配给角色,用户才能通过角色继承相应权限。 + +操作路径:「系统管理 → 角色管理」 + +1. 在角色列表中找到目标角色,点击「编辑」进入权限配置页面。 +2. 在权限树中展开「岗位管理」模块,勾选需要分配的权限节点(如「新增」「编辑」「删除」等)。 +3. 点击「保存」提交配置,权限即生效。 + +:::warning 特别说明 +注意:权限分配后,拥有该角色的用户需重新登录或刷新页面才能获取最新权限。若权限未生效,请检查用户是否已正确关联该角色。 +::: + +## 权限校验流程 + +``` +用户请求 → login_required(JWT 校验) → permission_required(权限校验) + ├── 用户 ID = 1(admin)→ 跳过权限校验,直接放行 + └── 其他用户 → 查询角色权限列表 → 匹配权限字符串 → 放行 / 拒绝 +``` + +## SQL 直接插入权限节点 + +如果需要通过 SQL 直接插入权限节点(如初始化脚本),参考以下语句: + +```sql +-- 假设岗位管理菜单 ID 为 100 +INSERT INTO tornado_menu (name, parent_id, path, component, perms, type, icon, sort, create_time, is_delete) +VALUES +('岗位查询', 100, NULL, NULL, 'sys:position:page', '1', NULL, 0, NOW(), 0), +('岗位新增', 100, NULL, NULL, 'sys:position:add', '1', NULL, 0, NOW(), 0), +('岗位编辑', 100, NULL, NULL, 'sys:position:update', '1', NULL, 0, NOW(), 0), +('岗位删除', 100, NULL, NULL, 'sys:position:delete', '1', NULL, 0, NOW(), 0), +('岗位批量删除', 100, NULL, NULL, 'sys:position:batchDelete', '1', NULL, 0, NOW(), 0), +('岗位状态', 100, NULL, NULL, 'sys:position:status', '1', NULL, 0, NOW(), 0); +``` + +:::danger 特别说明 +其中 `type='1'` 表示权限类型节点(非菜单)。 +::: + +## 开发要点 + +1. 权限字符串必须与代码中的 `@permission_required` 完全一致。 +2. admin 用户(ID=1)自动跳过权限校验:开发阶段可用 admin 测试。 +3. 权限节点是菜单的子节点:type 为 `1`(权限),不是 `0`(菜单)。 +4. 查询权限可适当合并:减少配置复杂度。 +5. 写操作权限建议独立:便于细粒度控制。 +## 总结 + +权限节点是菜单的子节点(type=1),命名规范为 `sys:{模块}:{操作}`。后端通过 `@permission_required` 装饰器校验,前端通过 `v-perm` 指令控制按钮显示。查询权限可适当合并,写操作权限建议独立以实现细粒度控制。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume2/repository.md b/DjangoAdmin_Tornado_EleVue/zh/volume2/repository.md new file mode 100644 index 0000000..e03b9b2 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume2/repository.md @@ -0,0 +1,102 @@ +# Repository 层 + +Repository 层是数据访问层,封装所有数据库查询操作。项目通过 `BaseRepository` 基类提供通用 CRUD 能力,子类通常只需声明类名,无需编写任何代码。 + +## 文件位置 + +``` +src/modules/position/repository.py +``` + +## 完整代码 + +```python +# +====================================================================== +# | 模块: 岗位数据访问层 +# | 说明: 定义岗位仓储,继承 BaseRepository 获得通用 CRUD 与软删除能力 +# +====================================================================== +"""岗位数据访问。""" + +from core.base_repository import BaseRepository +from modules.position.models import Position + + +# ====================================================================== +# 岗位仓储 +# ====================================================================== +class PositionRepository(BaseRepository): + """岗位仓储。""" + + # ============================================================ + # 绑定的模型类 + # ============================================================ + model = Position +``` + +## 代码解析 + +### 基类继承 + +```python +class PositionRepository(BaseRepository(Position]): +``` + +`BaseRepository(Position]` 是泛型基类,传入模型类型后自动获得以下能力: + +| 方法 | 说明 | +|:----|:----| +| `get_by_id(id)` | 根据 ID 查询单条(自动过滤软删除) | +| `get_by_ids(ids)` | 根据 ID 列表查询多条 | +| `get_one(**kwargs)` | 按等值条件查询单条(跳过 None 值) | +| `all()` | 按等值条件查询全部(自动过滤软删除) | +| `exists_by_field(field, value)` | 判断字段值是否存在 | +| `count(**kwargs)` | 按条件计数 | +| `filter(*criterion)` | 原始 SQLAlchemy 条件查询(不自动过滤软删除) | +| `filter_by(**kwargs)` | 等值条件查询(自动过滤软删除,跳过 None) | +| `create(**kwargs)` | 创建记录 | +| `update(id, **kwargs)` | 更新记录 | +| `paginate(page, limit)` | 分页查询 | +| `batch_delete(ids)` | 批量软删除 | + +### Service 层使用方式 + +Repository 不需要手动创建单例。Service 层通过 `repo_cls = PositionRepository` 声明关联的 Repository 类,`BaseService.__init__` 会自动实例化: + +```python +# service.py 中的使用方式 +class PositionService(BaseService): + model = Position + repo_cls = PositionRepository # 传入类,基类自动实例化 +``` + +### 何时需要自定义方法 + +大多数简单 CRUD 模块(如 Position、Level、Category)的 Repository 只需 `pass`。当需要以下场景时,才在子类中添加自定义方法: + +- **复杂联表查询**:如 User 需要关联 Dept、Role 查询 +- **特殊聚合查询**:如统计、分组 +- **非标准查询条件**:如区间查询、模糊 + 精确组合 + +示例(UserRepository 中的自定义方法): + +```python +class UserRepository(BaseRepository(User]): + def get_by_username(self, username: str): + """根据用户名查询用户(含角色信息)""" + return self.filter( + User.username == username, + User.is_delete == 0 + ).first() +``` + +## 开发要点 + +1. **简单模块只需声明 `model`**:继承基类即可获得完整 CRUD 能力 +2. **Service 层通过 `repo_cls` 关联 Repository 类**:基类自动实例化,无需手动创建单例 +3. **`filter()` 不自动过滤软删除**:需要手动追加 `Model.is_delete == 0` 条件 +4. **`filter_by()` / `get_one()` / `all()` 自动过滤软删除**:且跳过 None 值,推荐优先使用 +5. **Repository 层不做响应封装**:返回 SQLAlchemy 查询结果,由 Service 层处理响应 + +## 总结 + +Repository 层继承 `BaseRepository` 获得通用 CRUD 能力,等值查询方法(`get_one`/`get_all`/`filter_by`)自动过滤软删除且跳过 None 值,`filter()` 为原始出口需手动追加软删除条件。Repository 层不做响应封装,由 Service 层处理。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume2/router.md b/DjangoAdmin_Tornado_EleVue/zh/volume2/router.md new file mode 100644 index 0000000..fdea8df --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume2/router.md @@ -0,0 +1,125 @@ +# 路由注册 + +路由注册是将模块的 Handler 挂载到应用的过程。所有模块的路由在 `src/api/v1/router.py` 中统一注册。 + +## 文件位置 + +``` +src/api/v1/router.py +src/api/v1/{module}.py # 每个模块的路由定义 +``` + +## 注册步骤 + +### 第一步:定义模块路由 + +在 `src/api/v1/position.py` 中定义 routes 列表: + +```python +# +====================================================================== +# | 模块: 岗位路由层 +# | 说明: 提供岗位管理的增删改查、批量删除、状态管理等 API 接口 +# +====================================================================== +"""岗位相关路由。""" + +from modules.position.handlers import ( + PositionAddHandler, PositionBatchDeleteHandler, PositionDeleteHandler, + PositionDetailHandler, PositionGetListHandler, PositionPageHandler, + PositionStatusHandler, PositionUpdateHandler, +) + +routes = [ + # 查询岗位分页数据 + (r"/position/page", PositionPageHandler), + # 查询岗位详情 + (r"/position/detail/(\d+)", PositionDetailHandler), + # 获取岗位下拉列表 + (r"/position/getPositionList", PositionGetListHandler), + # 添加岗位 + (r"/position/add", PositionAddHandler), + # 更新岗位 + (r"/position/update", PositionUpdateHandler), + # 删除岗位 + (r"/position/delete/(\d+)", PositionDeleteHandler), + # 设置岗位状态 + (r"/position/status", PositionStatusHandler), + # 批量删除岗位 + (r"/position/batchDelete", PositionBatchDeleteHandler), +] +``` + +### 第二步:在 router.py 中注册 + +在 `src/api/v1/router.py` 的 `_RESOURCE_MODULES` 列表中追加模块: + +```python +# src/api/v1/router.py +from api.v1 import (auth, index, upload, user, + user_role, role, role_menu, menu, + dept, dict, dict_item, config, + config_item, level, position, city, + notice, link, category, article, + param, file_template, job, generator, + operation_log, login_log, job_log, example) + +from api.v1 import position # 新增 + +_RESOURCE_MODULES = [ + auth, index, upload, user, + # ... 已有模块 ... + position, # 新增 +] +``` + +:::tip 注册规则 +新模块只需两步: +1. 在 `src/api/v1/` 下创建路由文件,定义 `routes` 列表 +2. 在 `router.py` 的 `_RESOURCE_MODULES` 中追加模块导入 + +路由前缀 `/api/v1` 由 `build_routes()` 自动拼接,无需手动添加。 +::: + +## 最终 API 路径 + +注册后,Position 模块的接口路径为: + +| 方法 | 路径 | 说明 | +|:----|:----|:----| +| GET | `/api/v1/position/page` | 分页查询 | +| GET | `/api/v1/position/detail/(\d+)` | 详情查询 | +| GET | `/api/v1/position/getPositionList` | 下拉列表 | +| POST | `/api/v1/position/add` | 新增 | +| PUT | `/api/v1/position/update` | 更新 | +| DELETE | `/api/v1/position/delete/(\d+)` | 单条删除 | +| POST | `/api/v1/position/batchDelete` | 批量删除 | +| PUT | `/api/v1/position/status` | 状态变更 | + +## URL 命名规范 + +| 规则 | 示例 | +|:----|:----| +| 使用小写英文 | `/position` 而非 `/Position` | +| 使用单数形式 | `/position` 而非 `/positions` | +| 多单词用驼峰 | `/fileTemplate` 或 `/file-template` | +| 路径参数用正则 | `(\d+)` 匹配数字 ID | + +## build_routes 工作原理 + +```python +# src/api/v1/router.py +V1_PREFIX = "/api/v1" + +def build_routes() -> list: + """构建全部路由(含 /api/v1 前缀与健康检查)。""" + routes = [] + for module in _RESOURCE_MODULES: + routes.extend([(V1_PREFIX + path, handler) for path, handler in module.routes]) + routes.append((V1_PREFIX + "/health", HealthHandler)) + return routes +``` + +`build_routes()` 遍历所有模块的 `routes` 列表,为每条路由拼接 `/api/v1` 前缀,最后追加健康检查端点。 + +## 总结 + +路由注册采用声明式模式:每个模块在 `src/api/v1/{module}.py` 中定义 `routes` 列表,在 `router.py` 的 `_RESOURCE_MODULES` 中追加导入即可。路由前缀 `/api/v1` 自动拼接,无需手动配置。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume2/schema.md b/DjangoAdmin_Tornado_EleVue/zh/volume2/schema.md new file mode 100644 index 0000000..4069def --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume2/schema.md @@ -0,0 +1,125 @@ +# Pydantic Schema + +Schema 层负责请求数据的校验。项目使用 Pydantic v2 的 `BaseModel` 定义表单类,通过 `Field` 约束字段格式,Tornado 在接收请求体时自动执行校验。 + +## 文件位置 + +``` +src/modules/position/schemas.py +``` + +## 完整代码 + +```python +from typing import Optional + +from pydantic import BaseModel, Field +from core.base_schema import BaseSchema + + +# ============================================================ +# 岗位表单类 +# ============================================================ +class PositionForm(BaseSchema): + """岗位创建/编辑表单""" + name: str = Field(..., min_length=1, max_length=150, description="岗位名称") + status: int = Field(..., ge=1, le=2, description="岗位状态:1-在用 2-停用") + sort: int = Field(..., ge=0, le=99999, description="岗位排序") + + +# ============================================================ +# 设置状态表单类 +# ============================================================ +class PositionStatusForm(BaseModel): + """岗位状态更新表单""" + id: int = Field(..., gt=0, description="岗位ID") + status: int = Field(..., ge=1, le=2, description="岗位状态:1-在用 2-停用") +``` + +## 代码解析 + +### 基类选择 + +| 基类 | 用途 | 包含字段 | +|:----|:----|:----| +| `BaseSchema` | 创建/编辑通用表单 | 自带可选 `id` 字段(`Optional[int]`),编辑时传入 | +| `BaseModel` | 特定操作表单 | 无额外字段,按需定义 | + +创建和编辑共用 `PositionForm`:创建时不传 `id`,编辑时传入 `id`。`BaseSchema` 的 `id` 字段定义为 `Optional[int] = Field(None, gt=0)`,创建时可省略。 + +### Field 约束 + +```python +name: str = Field(..., min_length=1, max_length=150, description="岗位名称") +``` + +| 参数 | 含义 | +|:----|:----| +| `...` | 必填(Ellipsis) | +| `min_length=1` | 最小长度 1,不允许空字符串 | +| `max_length=150` | 最大长度 150 | +| `description` | Swagger 文档中的字段说明 | + +```python +status: int = Field(..., ge=1, le=2, description="岗位状态:1-在用 2-停用") +``` + +| 参数 | 含义 | +|:----|:----| +| `ge=1` | 大于等于(Greater Equal) | +| `le=2` | 小于等于(Less Equal) | + +```python +sort: int = Field(..., ge=0, le=99999, description="岗位排序") +``` + +排序字段允许 0 到 99999 的范围。 + +### 状态更新单独建类 + +```python +# ============================================================ +# 设置状态表单类 +# ============================================================ +class PositionStatusForm(BaseModel): + """岗位状态更新表单""" + id: int = Field(..., gt=0, description="岗位ID") + status: int = Field(..., ge=1, le=2, description="岗位状态:1-在用 2-停用") +``` + +状态更新接口只需要 `id` 和 `status` 两个字段,与创建/编辑表单分开定义,接口语义更清晰。 + +## 如何在 Service 中使用 + +Schema 在 Service 层通过 `create_schema` / `update_schema` / `status_schema` 声明,`BaseService` 的 `add()` / `update()` / `update_status()` 方法自动执行校验: + +```python +class PositionService(BaseService): + model = Position + repo_cls = PositionRepository + create_schema = PositionSchema # 新增/编辑时校验 + update_schema = PositionSchema # 编辑时校验 + status_schema = PositionStatusSchema # 状态变更时校验 +``` + +校验由 `BaseService.validate()` 方法内部调用 `schema_cls.model_validate(data)` 完成。校验失败时抛出 `ValidationError`,由 `BaseHandler.write_error` 统一转换为标准错误响应: + +```json +{ + "code": 1, + "data": null, + "msg": "name: String should have at least 1 character", + "ok": false +} +``` + +## 开发要点 + +1. **创建/编辑共用一个表单类**:继承 `BaseSchema`,利用其内置的可选 `id` 字段 +2. **特殊操作单独建类**:如状态更新、排序调整等,只包含必要的字段 +3. **合理设置约束**:`min_length` / `max_length` / `ge` / `le` / `gt` 等,让框架自动校验 +4. **description 建议填写**:用于字段说明,方便团队理解字段含义 + +## 总结 + +Pydantic Schema 通过 `BaseSchema` 继承公共字段(id),使用 `Field(...)` 声明约束。在 Service 层通过 `create_schema` / `update_schema` / `status_schema` 声明,`BaseService` 自动执行校验。特殊操作单独建类。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume2/service.md b/DjangoAdmin_Tornado_EleVue/zh/volume2/service.md new file mode 100644 index 0000000..c4a073e --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume2/service.md @@ -0,0 +1,145 @@ +# Service 层 + +Service 层是业务逻辑层,负责实现具体的业务规则。简单 CRUD 模块继承 `BaseService` 基类,只需声明差异点即可获得完整的增删改查能力。 + +## 文件位置 + +``` +src/modules/position/service.py +``` + +## 完整代码 + +```python +# +====================================================================== +# | 模块: 岗位业务服务层 +# | 说明: 继承 BaseService 获得通用 CRUD,声明差异点 + 覆盖删除前钩子 +# +====================================================================== +"""岗位业务服务。""" + +from typing import Optional + +from common.utils.helpers import parse_id_list +from core.base_service import BaseService +from modules.position.models import Position +from modules.position.repository import PositionRepository +from modules.user.models import User +from modules.user.repository import UserRepository + + +class PositionService(BaseService(Position]): + """岗位业务服务类,继承基础服务获得通用 CRUD 能力。""" + + # ============================================================ + # 数据访问与模型 + # ============================================================ + repo_cls = PositionRepository + model = Position + + # ============================================================ + # 分页差异点 + # ============================================================ + page_like_fields = ('name',) + page_eq_fields = ('status',) + page_order_by = (('sort', 'asc'),) + + # ============================================================ + # 唯一性校验 + # ============================================================ + unique_fields = {'name': '岗位名称不能重复'} + + # ============================================================ + # 删除前校验 + # ============================================================ + def _before_delete(self, ids) -> Optional[str]: + """删除前校验:存在用户引用该岗位时禁止删除。""" + id_list = parse_id_list(str(ids)) + if id_list: + user_repo = UserRepository() + if user_repo.filter(User.position_id.in_(id_list), User.is_delete == 0).first(): + return "存在用户引用该岗位,请先调整用户岗位" + return None + + +# 模块级单例 +position_service = PositionService() +``` + +## 代码解析 + +### 必须声明的差异点 + +```python +repo_cls = PositionRepository # 关联的 Repository 类(不是实例) +model = Position # 关联的模型类 +``` + +:::tip 注意 +`repo_cls` 是 **类属性**,值为 Repository **类**(不是实例)。`BaseService.__init__` 会自动实例化它。 +::: + +### 分页查询配置 + +```python +page_like_fields = ('name',) # 模糊查询字段(LIKE) +page_eq_fields = ('status',) # 精确查询字段(=) +page_order_by = (('sort', 'asc'),) # 排序规则 +``` + +当调用 `get_page(handler)` 时,基类自动: +1. 从 `handler` 的查询参数读取 `pageNo`、`pageSize` +2. 对 `page_like_fields` 中的字段执行 `LIKE %keyword%` 查询 +3. 对 `page_eq_fields` 中的字段执行精确匹配 +4. 按 `page_order_by` 排序 +5. 返回分页数据 + +### 唯一性校验 + +```python +unique_fields = {'name': '岗位名称不能重复'} +``` + +字典的 key 是模型字段名,value 是校验失败时的错误提示。在 `add()` 和 `update()` 时,基类自动查询数据库判断是否重复。 + +### 生命周期钩子 + +```python +def _before_delete(self, ids) -> Optional[str]: +``` + +`BaseService` 提供以下钩子方法,子类可按需覆盖: + +| 钩子 | 触发时机 | 返回值 | +|:----|:----|:----| +| `_before_delete(ids)` | 删除操作前 | 返回错误字符串则中止删除,返回 None 则继续 | +| `_before_add(handler, data)` | 新增操作前 | 可修改 data(原地修改) | +| `_before_update(handler, data)` | 更新操作前 | 可修改 data(原地修改) | +| `_serialize(item)` | 列表序列化时 | 自定义返回字段 | +| `_serialize_detail(obj)` | 详情序列化时 | 自定义返回字段 | + +## 基类提供的通用方法 + +| 方法 | 说明 | +|:----|:----| +| `get_page(handler)` | 分页查询,返回 R 对象 | +| `get_detail(obj_id)` | 查询详情,返回序列化字典或 None | +| `add(handler)` | 新增记录,返回 R 对象 | +| `update(handler)` | 更新记录,返回 R 对象 | +| `delete(handler, ids)` | 单条/批量删除(软删除),返回 R 对象 | +| `batch_delete(handler)` | 批量删除(请求体为 ID 数组),返回 R 对象 | +| `update_status(handler)` | 更新状态,返回 R 对象 | +| `get_options(handler)` | 下拉列表查询,返回 R 对象 | + +## 开发要点 + +1. **必须声明 `repo_cls` 和 `model`**:基类所有方法依赖这两个属性 +2. **`repo_cls` 是类而非实例**:传入 Repository 类,基类自动实例化 +3. **分页配置按需声明**:`page_like_fields` / `page_eq_fields` / `page_order_by` +4. **唯一性校验通过 `unique_fields` 配置**:基类自动处理,无需手写查询 +5. **删除前校验用 `_before_delete` 钩子**:返回错误字符串中止操作 +6. **必须创建模块级单例**:`xxx_service = XxxService()` +7. **所有方法接收 `handler`(Tornado Handler 实例)**:非 request 对象 + +## 总结 + +Service 层继承 `BaseService` 获得通用 CRUD 能力,只需声明差异点(`repo_cls`/`model`/`page_like_fields`/`unique_fields`/`serialize_maps`)并覆写钩子方法。必须创建模块级单例,所有方法接收 Tornado Handler 实例。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume3/base-classes.md b/DjangoAdmin_Tornado_EleVue/zh/volume3/base-classes.md new file mode 100644 index 0000000..0878cf8 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume3/base-classes.md @@ -0,0 +1,530 @@ +# 核心基类体系 + +本章详细描述系统的核心基类体系,包括 `BaseModel`、`BaseSchema`、`BaseRepository`、`BaseService` 四大基类的设计理念、继承关系和使用方式。以岗位(Position)模块为实例贯穿说明。 + +## 基类继承关系 + +:::tip 基类体系总览 +四大基类各司其职,覆盖从数据模型到请求校验再到业务逻辑的完整链路。子类只需声明差异点,即可获得标准化的增删改查能力。 +::: + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ SQLAlchemy Base (declarative_base) │ +└──────────────────────────┬───────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────┐ +│ BaseModel (src/core/base_model.py) │ +│ ─────────────────────────────────────────────────────────────── │ +│ 通用字段: id / create_user / create_time / update_user / │ +│ update_time / is_delete │ +│ 序列化: to_dict() → 驼峰字段名 + UTC 转本地 + 排除敏感字段 │ +│ 子类钩子: __table_comment__ 自动写入 __table_args__ │ +└──────────────────────────┬───────────────────────────────────────┘ + │ + ▼ + Position(BaseModel) + ┌──────────────────────────┐ + │ __tablename__ │ + │ name / status / sort │ + └──────────────────────────┘ + +┌──────────────────────────────────────────────────────────────────┐ +│ BaseSchema (src/core/base_schema.py) │ +│ ─────────────────────────────────────────────────────────────── │ +│ Pydantic BaseModel 子类 │ +│ 可选 id 字段: Optional[int],创建时可省略 │ +│ 空字符串容错: '' / 'null' / 'undefined' → None │ +└──────────────────────────┬───────────────────────────────────────┘ + │ + ▼ + PositionSchema(BaseSchema) + ┌──────────────────────────┐ + │ name / status / sort │ + │ 继承可选 id │ + └──────────────────────────┘ + +┌──────────────────────────────────────────────────────────────────┐ +│ BaseRepository (src/core/base_repository.py) │ +│ ─────────────────────────────────────────────────────────────── │ +│ 查询: get_by_id / get_by_ids / get_one / all / count │ +│ 分页: paginate → (items, total) │ +│ 写操作: create / update / batch_delete │ +│ 条件: filter (原始) / filter_by (等值+软删) │ +│ 软删除: 等值方法自动追加 is_delete=0 │ +└──────────────────────────┬───────────────────────────────────────┘ + │ + ▼ + PositionRepository(BaseRepository) + ┌──────────────────────────┐ + │ model = Position │ + └──────────────────────────┘ + +┌──────────────────────────────────────────────────────────────────┐ +│ BaseService (src/core/base_service.py) │ +│ ─────────────────────────────────────────────────────────────── │ +│ 查询: get_page / get_options / get_detail │ +│ 写操作: add / update / delete / batch_delete / update_status │ +│ 差异点: repo_cls / model / page_like_fields / page_eq_fields / │ +│ page_order_by / unique_fields / serialize_maps │ +│ 钩子: _before_add / _before_update / _before_delete │ +│ _serialize / _serialize_detail │ +│ _build_create_fields / _build_update_fields │ +└──────────────────────────┬───────────────────────────────────────┘ + │ + ▼ + PositionService(BaseService) + ┌──────────────────────────────────┐ + │ repo_cls = PositionRepository │ + │ model = Position │ + │ page_like_fields = ('name',) │ + │ page_eq_fields = ('status',) │ + │ page_order_by = (('sort','asc'),)│ + │ unique_fields = {'name': '...'} │ + │ position_service = 单例 │ + └──────────────────────────────────┘ +``` + +## base_model — 模型基类 + +**文件位置**:`src/core/base_model.py` + +### 设计目标 + +为所有业务模型提供统一的通用字段和序列化方法,消除重复定义。 + +### 通用字段 + +```python +class BaseModel(Base): + __abstract__ = True # 抽象基类,不会在数据库中创建对应的表 + # 主键:自增整数,所有业务表的唯一标识 + id = Column(Integer, primary_key=True, autoincrement=True, comment="主键ID") + # 创建人:记录该条记录的创建者姓名,由 Repository 层自动填充 + create_user = Column(String(50), nullable=True, comment="创建人") + # 创建时间:记录创建时的 UTC 时间,数据库默认自动生成 + create_time = Column(DateTime, default=_UTC_NOW, server_default=func.now(), comment="创建时间") + # 更新人:记录最后修改该条记录的姓名,由 Repository 层自动填充 + update_user = Column(String(50), nullable=True, comment="更新人") + # 更新时间:记录最后修改时间,更新时自动刷新,数据库默认自动生成 + update_time = Column(DateTime, default=_UTC_NOW, onupdate=_UTC_NOW, server_default=func.now(), comment="最后更新时间") + # 逻辑删除标记:0 表示正常记录,1 表示已删除,物理删除不使用,保证数据可恢复;添加索引以提升查询性能,Repository 层默认过滤 is_delete=0 + is_delete = Column(Integer, default=0, server_default=text('0'), index=True, comment="逻辑删除标识:0-正常,1-已删除") +``` + +| 字段 | 类型 | 说明 | +|:---|:---|:---| +| `id` | Integer, PK, AI | 主键自增 | +| `create_user` | String(50) | 创建人(realname) | +| `create_time` | DateTime | 创建时间(UTC,server_default) | +| `update_user` | String(50) | 更新人(realname) | +| `update_time` | DateTime | 更新时间(UTC,onupdate 自动触发) | +| `is_delete` | Integer | 软删除标识:0=正常,1=已删除 | + +### 表注释钩子 + +```python +# src/core/base_model.py +# ============================================================ +# 子类钩子:自动将 __table_comment__ 写入 __table_args__ +# ============================================================ +def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + comment = getattr(cls, '__table_comment__', None) + if not comment: + return + # 仅对有 __tablename__ 的具体模型操作(跳过纯抽象中间层) + if not getattr(cls, '__tablename__', None): + return + existing = getattr(cls, '__table_args__', {}) + if isinstance(existing, dict): + merged = {**existing} + elif isinstance(existing, tuple) and existing: + merged = dict(existing[-1]) if isinstance(existing[-1], dict) else {} + else: + merged = {} + if 'comment' not in merged: + merged['comment'] = comment + cls.__table_args__ = merged +``` + +子类只需声明 `__table_comment__ = "岗位表"`,基类通过 `__init_subclass__` 钩子自动将其写入 `__table_args__`,无需手动配置 `Table(comment=...)`。 + +### 序列化方法 + +```python +# src/core/base_model.py +def to_dict(self) -> Dict[str, Any]: + """将模型实例转换为字典""" + result = {} + for column in self.__table__.columns: + if column.name in SERIALIZE_EXCLUDE_FIELDS: + continue + value = getattr(self, column.name) + if isinstance(value, datetime): + if value.tzinfo is None: + # MySQL DATETIME 无时区信息,读回为 naive UTC,统一按 UTC 转本地 + value = value.replace(tzinfo=timezone.utc) + value = value.astimezone().strftime('%Y-%m-%d %H:%M:%S') + elif isinstance(value, date): + value = value.strftime('%Y-%m-%d') + # 字段名转驼峰 + camel_key = snake_to_camel(column.name) + result[camel_key] = value + return result +``` + +关键设计点: + +1. **敏感字段排除**:`is_delete`、`password`、`salt` 不会出现在序列化结果中 +2. **时区处理**:MySQL 的 `DATETIME` 读回为 naive UTC,统一转 aware UTC 后再转本地时间 +3. **驼峰命名**:数据库下划线字段名自动转为前端习惯的驼峰命名 + +## BaseSchema — 表单基类 + +**文件位置**:`src/core/base_schema.py` + +### 设计目标 + +所有创建/编辑表单的 Pydantic 基类,提供统一的可选 `id` 字段和前端空字符串容错处理。 + +### 完整代码 + +```python +# src/core/base_schema.py +# +====================================================================== +# | 模块: 表单基类 +# | 说明: 统一处理前端传入的空字符串 id → None 等通用校验 +# +====================================================================== + +from typing import Optional +from pydantic import BaseModel, Field, field_validator + +# ============================================================ +# 表单基类 +# ============================================================ +class BaseSchema(BaseModel): + """ + 表单基类 + + 所有创建/编辑表单应继承此类,自动获得: + 1. id 字段:Optional[int],前端传空字符串时自动转为 None + """ + + id: Optional[int] = Field(None, description="主键ID") + + @field_validator('id', mode='before') + @classmethod + def empty_str_to_none(cls, v): + """前端 POST 空字符串 '' 时转为 None,避免 Pydantic 整数解析报错""" + if v == '' or v == 'null' or v == 'undefined': + return None + return v +``` + +### 代码解析 + +**可选 `id` 字段**: + +```python +id: Optional[int] = Field(None, description="主键ID") +``` + +`id` 默认为 `None`。创建记录时前端不传 `id`,Pydantic 自动赋默认值;编辑记录时前端传入 `id`,Service 层据此判断是新增还是更新。 + +**空字符串容错**: + +```python +@field_validator('id', mode='before') +@classmethod +def empty_str_to_none(cls, v): + if v == '' or v == 'null' or v == 'undefined': + return None + return v +``` + +前端表单在未填写 `id` 时,可能提交空字符串 `''`、字面量 `'null'` 或 `'undefined'`。Pydantic 无法将这些值解析为 `int`,会抛出校验错误。`mode='before'` 确保此验证器在 Pydantic 类型转换之前执行,将这些边界值统一转为 `None`。 + +### 子类示例 + +```python +# src/modules/position/schemas.py +# +====================================================================== +# | 模块: 岗位表单验证 +# | 说明: 岗位创建/编辑/状态更新的请求数据校验 +# +====================================================================== +from core.base_schema import BaseSchema + +# ============================================================ +# 岗位表单类 +# ============================================================ +class PositionForm(BaseSchema): + """岗位创建/编辑表单""" + name: str = Field(..., min_length=1, max_length=150, description="岗位名称") + status: int = Field(..., ge=1, le=2, description="岗位状态:1-在用 2-停用") + sort: int = Field(..., ge=0, le=99999, description="岗位排序") + +``` + +`PositionForm` 继承 `BaseSchema` 后自动获得可选 `id` 字段。创建时 JSON 请求体无需包含 `id`: + +```json +{ "name": "测试岗位", "status": 1, "sort": 0 } +``` + +编辑时传入 `id`: + +```json +{ "id": 1, "name": "修改后", "status": 1, "sort": 1 } +``` + +### 使用场景 + +| 场景 | 是否传 id | BaseSchema 行为 | +|:----|:----|:----| +| 新增 | 不传 | `id = None`,Service 走 `add()` 分支 | +| 编辑 | 传入 | `id = 123`,Service 走 `update()` 分支 | +| 特定操作 | 不需要 id | 继承 `BaseModel` 而非 `BaseSchema`(如 `PositionStatusForm`) | + +:::tip 何时继承 BaseSchema vs BaseModel +- **创建/编辑共用的表单**:继承 `BaseSchema`,自动获得可选 `id` +- **特定操作的表单**(如状态更新、排序调整):直接继承 `BaseModel`,只定义必要字段 +::: + +## BaseRepository — 仓库基类 + +**文件位置**:`src/core/base_repository.py` + +### 设计目标 + +封装各模型的数据访问层公共操作,提供统一的 CRUD 接口和软删除约定。 + +### 核心方法一览 + +| 方法 | 签名 | 说明 | 软删过滤 | +|:---|:---|:---|:---:| +| `get_by_id` | `(obj_id) → Optional[M]` | 按 ID 获取单条记录 | 是 | +| `get_by_ids` | `(ids) → list` | 按 ID 列表批量获取 | 是 | +| `get_one` | `(**filters) → Optional[M]` | 等值条件获取单条 | 是 | +| `all` | `(**filters) → List[M]` | 等值条件获取所有 | 是 | +| `count` | `(**filters) → int` | 等值条件计数 | 是 | +| `exists` | `(obj_id) → bool` | 检查记录是否存在 | 是 | +| `exists_by_field` | `(field, value, exclude_id) → bool` | 唯一性校验 | 是 | +| `paginate` | `(page_no, page_size, query) → (items, total)` | 分页查询 | 是 | +| `filter` | `(*criterion) → Query` | 原始条件查询 | **否** | +| `filter_by` | `(**kwargs) → Query` | 等值条件查询 | 是 | +| `create` | `(**fields) → M` | 创建记录 | — | +| `update` | `(obj_id, data) → int` | 按 ID 更新 | 是 | +| `batch_delete` | `(ids_str) → int` | 按 ID 软删除 | — | + +### 软删除约定 + +:::warning 重要设计约定 +- **等值便捷方法**(get_by_id / get_one / all / count / filter_by / exists / exists_by_field / paginate)自动追加 `is_delete=0` 过滤,调用方无需关心 +- **原始出口** `filter(*criterion)` **不做**软删过滤,由调用方自行追加 `Model.is_delete == 0`,用于 `in_` / `!=` / `and_` / 区间等复杂条件 +::: + +### 数据清洗 + +```python +# src/core/base_repository.py +def _clean_model_data(self, data: dict) -> dict: + """清洗入库数据:camelCase 键转 snake_case 并仅保留模型真实列字段 + + Pydantic schema 可能用 camelCase(如 parentId)而模型列是 snake_case + (parent_id),直传 Query.update / model(**fields) 会触发 CompileError / + TypeError。统一在数据访问层转换,业务模块无需各自处理。 + """ + if not data: + return {} + columns = {c.name for c in self.model.__table__.columns} + return { + _camel_to_snake(k): v + for k, v in data.items() + if _camel_to_snake(k) in columns + } +``` + +Pydantic Schema 可能用 camelCase(如 `parentId`),而模型列是 snake_case(如 `parent_id`)。`_clean_model_data` 在数据访问层统一转换,同时过滤掉模型中不存在的字段,业务模块无需各自处理。`_camel_to_snake` 是 `BaseRepository` 的静态方法。 + +## BaseService — 服务基类 + +**文件位置**:`src/core/base_service.py` + +### 设计目标 + +通过模板方法模式封装通用 CRUD 流程。子类只需声明差异点,即可获得分页、详情、添加、编辑、删除、批量删除、状态切换等标准接口。 + +### 差异点声明 + +| 差异点 | 类型 | 说明 | +|:---|:---|:---| +| `repo_cls` | `Type[BaseRepository]` | **必填**,数据仓库类(不是实例) | +| `model` | `type[M]` | **必填**,ORM 模型类 | +| `page_like_fields` | `Tuple[str, ...]` | 分页模糊查询字段(取同名查询参数,`LIKE %值%`) | +| `page_eq_fields` | `Tuple[str, ...]` | 分页等值查询字段(取同名查询参数,`=值`) | +| `page_order_by` | `Tuple[Tuple[str, str], ...]` | 分页排序规则,默认 `(('id', 'desc'),)` | +| `unique_fields` | `Dict[str, str]` | 唯一性校验字段及提示,如 `{'name': '岗位名称不能重复'}` | +| `serialize_maps` | `Dict[str, Dict]` | 枚举显示名映射。值为 `str` 时从数据字典取 `{value: label}`,值为 `dict` 时直接按值取名称 | +| `serialize_extra` | `Callable` | 列表/详情共用序列化钩子,返回需合并进字典的额外字段 | +| `detail_serialize` | `Callable` | 详情专用序列化钩子,仅在 `get_detail` 时合并 | +| `file_fields` | `Tuple[str, ...]` | 文件字段(add/update 时自动迁移临时文件,serialize 时自动补全 URL) | +| `rich_text_fields` | `Tuple[str, ...]` | 富文本字段(含嵌入图片迁移 + XSS 清洗) | +| `file_dir` | `str` | 文件存储子目录名,默认从表名去掉 `DB_PREFIX` 推导 | + +### 标准接口 + +| 方法 | 说明 | 返回值 | +|:---|:---|:---| +| `get_page(handler)` | 分页查询 | `R.page(data, total, current, size)` | +| `get_options(handler)` | 下拉列表查询 | `R.ok(data=[...])` | +| `get_detail(obj_id)` | 详情查询 | `dict` 或 `None` | +| `add(handler)` | 新增记录 | `R.ok(msg='添加成功')` | +| `update(handler)` | 编辑记录 | `R.ok(msg='更新成功')` | +| `delete(handler, ids)` | 删除记录 | `R.ok(msg='本次共删除N条数据')` | +| `batch_delete(handler)` | 批量删除 | `R.ok(...)` | +| `update_status(handler)` | 状态切换 | `R.ok(msg='设置成功')` | + +### 钩子方法 + +| 钩子 | 触发时机 | 默认行为 | +|:---|:---|:---| +| `_before_add(handler, data)` | 新增前 | 空操作 | +| `_before_update(handler, data)` | 编辑前 | 空操作 | +| `_before_delete(ids)` | 删除前 | 返回 None(放行) | +| `_build_page_query(handler)` | 分页查询构建 | 按声明字段过滤 + 排序 | +| `_serialize(item)` | 列表行序列化 | `to_dict()` + 枚举映射 + 文件 URL | +| `_serialize_detail(obj)` | 详情序列化 | 同列表 + `detail_serialize` | +| `_build_create_fields(data)` | 新增字段组装 | camelCase→snake_case + 过滤非模型字段 | +| `_build_update_fields(data)` | 更新字段组装 | 同上 + 排除 None 值 | + +### 删除流程 + +Service 层的 `delete()` 和 `batch_delete()` 方法内部调用 `repo.batch_delete(ids)` 完成软删除: + +```python +# src/core/base_service.py +async def delete(self, handler, ids) -> R: + try: + id_list = list(dict.fromkeys(parse_id_list(str(ids)))) + if not id_list: + return R.failed(handler, "记录ID不存在") + if len(self.repo.get_by_ids(id_list)) != len(id_list): + return R.failed(handler, "记录不存在") + err = self._before_delete(ids) + if err: + return R.failed(handler, err) + count = self.repo.batch_delete(ids) + return R.ok(handler, msg="本次共删除{0}条数据".format(count)) + finally: + self.repo.close() +``` + +:::tip 设计要点 +Repository 层的 `batch_delete` 只返回删除条数(数据原语),Service 层负责 R 响应封装和文案统一。删除前先校验全部记录存在,避免"部分软删已提交却整体报失败"导致的数据不一致。 +::: + +## 实例:Position 模块的基类使用 + +### Model(继承 BaseModel) + +```python +# src/modules/position/models.py +from sqlalchemy import Column, Integer, String, text + +from config.database import DB_PREFIX +from core.base_model import BaseModel + + +class Position(BaseModel): + """岗位模型。""" + __tablename__ = DB_PREFIX + "position" + __table_comment__ = "岗位表" + + # 岗位名称 + name = Column(String(255), nullable=False, index=True, comment="岗位名称") + # 岗位状态:1-在用 2-停用 + status = Column(Integer, default=0, server_default=text('0'), index=True, comment="岗位状态") + # 岗位排序 + sort = Column(Integer, default=0, server_default=text('0'), comment="岗位排序") +``` + +获得的能力: +- 6 个通用字段(id / create_user / create_time / update_user / update_time / is_delete) +- `to_dict()` 序列化方法(自动排除 password/salt/is_delete,驼峰命名) +- `__table_comment__` 自动写入 `__table_args__` + +### Repository(继承 BaseRepository) + +```python +# src/modules/position/repository.py +"""岗位数据访问。""" + +from core.base_repository import BaseRepository +from modules.position.models import Position + + +class PositionRepository(BaseRepository): + """岗位仓储。""" + model = Position +``` + +获得的能力: +- 全部通用 CRUD 方法(get_by_id / get_by_ids / get_one / all / count / exists / exists_by_field / paginate / filter / filter_by / create / update / batch_delete) +- 软删除自动过滤(等值方法自动追加 `is_delete=0`) +- camelCase → snake_case 数据清洗(`_clean_model_data`) + +### Service(继承 BaseService) + +```python +# src/modules/position/service.py +"""岗位业务逻辑。""" + +from core.base_service import BaseService +from modules.position.models import Position +from modules.position.repository import PositionRepository +from modules.position.schemas import PositionSchema, PositionStatusSchema + + +class PositionService(BaseService): + """岗位服务。""" + # 基础声明 + model = Position + repo_cls = PositionRepository + # 差异点声明 + create_schema = PositionSchema + update_schema = PositionSchema + status_schema = PositionStatusSchema + page_like_fields = ("name",) + page_eq_fields = ("status",) + page_order_by = (("sort", "asc"),) + options_filter = {"status": 1} + + +# 模块级单例 +position_service = PositionService() +``` + +获得的能力: +- 8 个标准接口(get_page / get_options / get_detail / add / update / delete / batch_delete / update_status) +- 唯一性自动校验 +- 分页查询自动构建 +- 序列化自动处理 + +:::tip 极简 CRUD +对于最简单的模块(如 Level 职级),Repository 和 Service 只需声明差异点,无需编写任何自定义方法,即可获得完整的增删改查能力。 +::: + +## 总结 + +五大基类通过模板方法模式和声明式配置,将通用 CRUD 流程固化为标准实现: + +| 基类 | 职责 | 核心能力 | +|:----|:----|:----| +| `BaseModel` | 模型基类 | 通用字段、to_dict() 序列化、表注释钩子 | +| `BaseSchema` | 表单基类 | 可选 id、空字符串容错 | +| `BaseRepository` | 仓库基类 | 通用 CRUD 方法、软删除自动过滤 | +| `BaseService` | 服务基类 | 标准接口、钩子方法、声明式配置 | + +子类只需声明差异点(过滤字段、排序规则、唯一性约束等),即可获得企业级的增删改查能力。以岗位模块为例,完整实现只需约 80 行有效代码(不含版权头),其中 Repository 几乎零代码。这种设计使得新模块的开发效率提升 3-5 倍,同时保证了代码风格和行为的一致性。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume3/cache.md b/DjangoAdmin_Tornado_EleVue/zh/volume3/cache.md new file mode 100644 index 0000000..fcae6f1 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume3/cache.md @@ -0,0 +1,350 @@ +# 缓存架构设计 + +本章详细描述 的缓存架构设计,包括两级缓存策略(进程内缓存 + Redis 缓存)、缓存应用场景、失效策略和代码实现。 + +## 两级缓存架构 + +:::tip 缓存分层设计 +采用两级缓存架构:第一级为进程内字典缓存(`dict_util`),第二级为 Redis 分布式缓存。两级缓存配合使用,在保证数据一致性的同时最大化查询性能。 +::: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 请求处理流程 │ +│ │ +│ ┌──────────┐ 命中 ┌──────────┐ 命中 ┌───────────┐ │ +│ │ 业务代码 │ ─────────→ │ L1 缓存 │ ────────→ │ 返回结果 │ │ +│ │ │ │ (进程内) │ │ │ │ +│ └──────────┘ └──────────┘ └───────────┘ │ +│ │ │ 未命中 │ +│ │ ▼ │ +│ │ ┌──────────┐ 命中 ┌───────────┐ │ +│ │ │ L2 缓存 │ ────────→ │ 写回 L1 │ │ +│ │ │ (Redis) │ │ 返回结果 │ │ +│ │ └──────────┘ └───────────┘ │ +│ │ │ 未命中 │ +│ │ ▼ │ +│ │ ┌──────────┐ ┌───────────┐ │ +│ │ │ 数据库 │ ────────→ │ 写回 L1 │ │ +│ │ │ (MySQL) │ │ 写回 L2 │ │ +│ │ └──────────┘ └───────────┘ │ +│ │ │ +│ ┌──────────┐ │ +│ │ 缓存失效 │ → 清除 L1 + 删除 L2 │ +│ └──────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 缓存层级对比 + +| 维度 | L1 进程内缓存 | L2 Redis 缓存 | +|:---|:---|:---| +| **存储位置** | Python 进程内存 | Redis 服务器 | +| **访问速度** | 纳秒级(字典查找) | 毫秒级(网络 I/O) | +| **数据共享** | 仅当前进程 | 所有进程/实例共享 | +| **持久化** | 进程重启丢失 | 持久化可配置 | +| **容量限制** | 受进程内存限制 | 受 Redis 内存限制 | +| **适用场景** | 高频读取、低变更数据 | 跨进程共享、分布式场景 | + +## 进程内字典缓存 + +### 设计决策 + +`dict_util` 采用**纯进程内 TTL 缓存**(无 Redis L2),原因: + +1. 字典数据几乎不变,读写均落在同步 service 路径,无法使用请求级异步 Redis +2. 字典/字典项增删改时由 service 主动失效,TTL 仅作兜底 +3. 多 worker 部署下各进程缓存最多在 TTL 内短暂不一致,属可接受范围 + +### 实现原理 + +```python +# src/common/utils/dict_util.py +import logging +import time + +from modules.dictionary.dict.models import Dict +from modules.dictionary.dict_item.models import DictItem +from modules.dictionary.dict.repository import dict_repo +from modules.dictionary.dict_item.repository import dictitem_repo + +logger = logging.getLogger(__name__) + +# 字典缓存 TTL(秒):兜底过期时间,实际变更靠主动失效即时生效 +_CACHE_TTL = 60 +# 进程内字典缓存:{code: (过期时间戳, 字典项列表)} +_dict_cache: dict = {} + + +def get_dict_items(code): + """按字典编码读取字典项列表 [{label, value, sort}, ...],字典不存在返回空列表""" + now = time.time() + hit = _dict_cache.get(code) + if hit and hit[0] > now: + return hit[1] + items = _load_dict_items(code) + _dict_cache[code] = (now + _CACHE_TTL, items) + return items + + +def get_dict_map(code): + """按字典编码读取 {value: label} 映射(value 为字典项存储值,字符串类型)""" + return {item['value']: item['label'] for item in get_dict_items(code)} + + +def get_dict_label(code, value, default=None): + """取单个字典项名称:value 自动转字符串匹配存储值,查不到返回 default""" + return get_dict_map(code).get(str(value), default) + + +def invalidate_dict(code): + """清除指定字典编码的缓存(None/未缓存时安全无操作)""" + if code: + _dict_cache.pop(code, None) + + +def invalidate_dicts_by_ids(dict_ids): + """按字典ID批量清除缓存:字典项变更后据 dict_id 反查 code 再失效""" + if not dict_ids: + return + try: + rows = dict_repo.filter(Dict.id.in_(dict_ids), Dict.is_delete == 0).all() + for row in rows: + invalidate_dict(row.code) + except Exception as e: + logger.warning("字典缓存失效失败(将按 TTL 自动过期): %s", e) + + +def _load_dict_items(code): + """从数据库读取字典项(未删除、按 sort+id 升序),字典不存在返回空列表""" + dict_obj = dict_repo.get_one(code=code) + if not dict_obj: + return [] + rows = dictitem_repo.filter_by(dict_id=dict_obj.id).all() + rows.sort(key=lambda item: (item.sort, item.id)) + return [{'label': item.name, 'value': item.value, 'sort': item.sort} for item in rows] +``` + +### 公开接口 + +| 函数 | 签名 | 说明 | +|:---|:---|:---| +| `get_dict_items` | `(code) → [{label, value, sort}]` | 字典项列表(供前端下拉) | +| `get_dict_map` | `(code) → {value: label}` | 值→显示名映射(供后端翻译) | +| `get_dict_label` | `(code, value, default) → str` | 单值翻译 | +| `invalidate_dict` | `(code) → None` | 按字典编码清除缓存 | +| `invalidate_dicts_by_ids` | `(dict_ids) → None` | 按字典 ID 批量清除缓存 | + +### 使用场景 + +| 场景 | 调用方式 | 说明 | +|:---|:---|:---| +| 枚举显示名 | `get_dict_map('status_type')` | 序列化时将值转为显示名 | +| 表单下拉 | `get_dict_items('gender')` | 前端获取字典项列表 | +| 单值翻译 | `get_dict_label('status', 1)` | 将 1 转为 "正常" | +| 业务校验 | `value in get_dict_map('xxx')` | 校验值是否在字典范围内 | + +### 缓存失效 + +字典数据变更时(新增/编辑/删除字典项),通过 `invalidate_dicts_by_ids` 按字典 ID 批量清除: + +```python +# src/modules/dictionary/dict_item/service.py +class DictItemService(BaseService(DictItem]): + repo = dictitem_repo + model = DictItem + + def add(self, request, data): + """新增字典项,新增后失效所属字典的缓存""" + result = super().add(request, data) + if data.dictId: + invalidate_dicts_by_ids({data.dictId}) + return result + + def update(self, request, data): + """更新字典项,更新后失效新旧所属字典的缓存""" + old = self.repo.get_by_id(data.id) if data.id else None + result = super().update(request, data) + dict_ids = {data.dictId} if data.dictId else set() + if old and old.dict_id: + dict_ids.add(old.dict_id) + invalidate_dicts_by_ids(dict_ids) + return result + + def delete(self, ids): + """删除字典项,删除后失效所属字典的缓存""" + from utils.request import parse_id_list + id_list = parse_id_list(str(ids)) + dict_ids = set() + if id_list: + for row in dictitem_repo.filter(DictItem.id.in_(id_list), DictItem.is_delete == 0).all(): + if row.dict_id: + dict_ids.add(row.dict_id) + result = super().delete(ids) + invalidate_dicts_by_ids(dict_ids) + return result +``` + +## L2 缓存:Redis 缓存 + +### Redis 连接管理 + +```python +# src/core/redis.py +import redis +from config.redis import REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, REDIS_AUTH + +# 同步 Redis 客户端(全局单例) +_redis_client = redis.Redis( + host=REDIS_HOST, + port=REDIS_PORT, + password=REDIS_PASSWORD if REDIS_AUTH else None, + decode_responses=True, +) + +redis = _redis_client +``` + +- 全局单例,所有请求共享同一 Redis 客户端 +- 同步客户端,与全站同步 DB/Redis 调用风格一致 +- 连接池由 `redis-py` 内置管理 + +### 缓存应用场景 + +| 应用场景 | 数据结构 | Key 格式 | TTL | 说明 | +|:---|:---|:---|:---|:---| +| **JWT 黑名单** | String | `jwt:blacklist:{token_hash}` | Token 剩余有效期 | 登出时将 Token 加入黑名单 | +| **登录失败锁** | String | `login:lock:{username}` | 600 秒 | 连续 5 次失败后锁定 | +| **登录失败计数** | String (Lua) | `login:fail:{username}` | 300 秒 | 原子递增 + 首次设置 TTL | +| **滑动窗口限流** | String (Lua) | `rl:{ip}:{window_index}` | 2 倍窗口时长 | 滑动窗口计数器(前缀可配置) | +| **权限列表缓存** | String (JSON) | `perm:list:{user_id}:{version}` | 7200 秒 | 用户权限字符串列表(版本号策略) | +| **菜单缓存** | String (JSON) | `perm:menu:{user_id}:{version}` | 7200 秒 | 用户菜单树(版本号策略) | +| **用户有效状态** | String | `user:active:{user_id}` | 300 秒 | login_required 每请求校验 | +| **验证码** | String | `captcha:{uuid}` | 300 秒 | 图形验证码文本 | + +:::tip 字典缓存不在 Redis 中 +数据字典(`dict_util`)采用纯进程内 TTL 缓存,不经过 Redis。原因是字典读取落在同步 service 路径,无法使用请求级异步 Redis 客户端。详见上方「进程内字典缓存」章节。 +::: + +### Lua 脚本 + +#### 原子递增 + 过期 + +```python +# src/core/redis.py +_INCR_EXPIRE_LUA = """ +local count = redis.call('incr', KEYS[1]) +if count == 1 then + redis.call('expire', KEYS[1], ARGV[1]) +end +return count +""" + +async def incr_with_expire(redis, key: str, seconds: int) -> int: + """原子执行 incr,并在 key 首次创建时一并设置过期时间""" + return redis.eval(_INCR_EXPIRE_LUA, 1, key, seconds) +``` + +:::tip 为什么需要 Lua 脚本? +`incr` 和 `expire` 分开执行时,两步之间若进程崩溃,key 会以无 TTL 的状态永久残留,导致基于 Redis 的限流/登录锁定永久封禁。Lua 脚本保证原子性,消除此风险。 +::: + +#### 滑动窗口限流 + +```python +_SLIDING_WINDOW_LUA = """ +local cur = redis.call('incr', KEYS[1]) +if cur == 1 then + redis.call('expire', KEYS[1], ARGV[1]) +end +local prev = tonumber(redis.call('get', KEYS[2]) or '0') +local elapsed = tonumber(ARGV[3]) - tonumber(ARGV[2]) * tonumber(ARGV[1]) +local f = elapsed / tonumber(ARGV[1]) +if f < 0 then f = 0 elseif f > 1 then f = 1 end +local effective = prev * (1 - f) + cur +return effective +""" +``` + +滑动窗口限流通过当前窗口与上一窗口的计数按时间比例加权,消除固定窗口边界处的 2x 突发问题。 + +## 缓存策略对比 + +| 策略 | 说明 | 适用场景 | 应用 | +|:---|:---|:---|:---| +| **Cache-Aside** | 先查缓存,未命中查 DB,写回缓存 | 读多写少 | 数据字典、权限列表 | +| **Write-Through** | 写操作同时更新缓存 | 数据一致性要求高 | 字典项变更时主动失效 | +| **Write-Behind** | 写操作只更新缓存,异步写 DB | 写入性能要求高 | 未使用 | +| **TTL 过期** | 缓存自动过期 | 允许短暂不一致 | 登录锁、限流计数 | +| **主动失效** | 数据变更时主动清除缓存 | 数据一致性要求高 | 字典缓存、权限缓存 | + +## 缓存失效策略 + +### 主动失效 + +数据变更时主动清除相关缓存,保证数据一致性: + +```python +# 字典项变更 → 按字典 ID 批量清除字典缓存 +invalidate_dicts_by_ids(dict_ids) + +# 菜单变更 → 递增全局版本号,所有用户权限缓存自然失效 +await invalidate_menu_change() + +# 用户角色变更 → 删除该用户的权限和菜单缓存 +await invalidate_user_perms(user_id) + +# 角色菜单变更 → 清除该角色下所有用户的权限缓存 +await invalidate_role_perms(role_id) +``` + +### TTL 自动过期 + +对于允许短暂不一致的数据,使用 TTL 自动过期: + +``` +数据字典缓存:TTL 60 秒(1 分钟,兜底过期,实际变更靠主动失效即时生效) +登录失败计数:TTL 300 秒(5 分钟) +滑动窗口限流:TTL 2 倍窗口时长 +验证码:TTL 300 秒(5 分钟) +``` + +### 缓存穿透防护 + +:::warning 缓存穿透 +当查询一个不存在的字典编码时,缓存和数据库都不会命中,导致每次请求都穿透到数据库。`dict_util` 通过缓存空列表来防护:即使 `_load_dict_items` 返回空列表,也写入缓存,避免反复查询数据库。 +::: + +```python +def get_dict_items(code): + now = time.time() + hit = _dict_cache.get(code) + if hit and hit[0] > now: + return hit[1] + items = _load_dict_items(code) # 字典不存在时返回 [] + _dict_cache[code] = (now + _CACHE_TTL, items) # 空列表也缓存,防止穿透 + return items +``` + +## 缓存监控 + +Redis 缓存的运行状态可通过以下方式监控: + +1. **Redis INFO**:连接数、内存使用、命中率 +2. **应用日志**:缓存命中/未命中日志(DEBUG 级别) +3. **Redis TTL**:检查 key 的剩余有效期 + +```bash +# 查看 Redis 缓存统计 +redis-cli INFO stats | grep -E "keyspace_hits|keyspace_misses" + +# 查看指定 key 的 TTL +redis-cli TTL "dict:status_type" + +# 查看所有字典缓存 key +redis-cli KEYS "dict:*" +``` + +## 总结 + +采用进程内缓存(L1)+ Redis 缓存(L2)的两级缓存架构。L1 缓存提供纳秒级访问速度,适合高频读取的字典数据;L2 缓存提供跨进程数据共享,适合分布式场景。缓存失效采用主动失效 + TTL 过期的混合策略,数据变更时主动清除缓存保证一致性,允许短暂不一致的场景使用 TTL 自动过期。Lua 脚本保证 Redis 操作的原子性,防止进程崩溃导致的缓存异常。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume3/database-design.md b/DjangoAdmin_Tornado_EleVue/zh/volume3/database-design.md new file mode 100644 index 0000000..610c71e --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume3/database-design.md @@ -0,0 +1,478 @@ +# 数据库设计 + +本章详细描述 的数据库设计,包括核心表结构、通用字段规范、ER 关系和命名约定。 + +:::tip 设计原则 +所有业务表继承 `base_model` 基类,自动获得 6 个通用字段(id / create_user / create_time / update_user / update_time / is_delete)。表名使用 `DB_PREFIX` 前缀(默认 `tornado_`),软删除通过 `is_delete` 字段实现。 +::: + +## 通用字段规范 + +所有业务表共享以下通用字段,由 `base_model` 基类自动提供: + +| 字段名 | 类型 | 约束 | 默认值 | 说明 | +|:---|:---|:---|:---|:---| +| `id` | INT | PRIMARY KEY, AUTO_INCREMENT | — | 主键自增 | +| `create_user` | VARCHAR(50) | NULLABLE | NULL | 创建人(realname) | +| `create_time` | DATETIME | NOT NULL | CURRENT_TIMESTAMP | 创建时间(UTC) | +| `update_user` | VARCHAR(50) | NULLABLE | NULL | 更新人(realname) | +| `update_time` | DATETIME | NOT NULL | CURRENT_TIMESTAMP | 更新时间(UTC,onupdate) | +| `is_delete` | INT | NOT NULL, INDEX | 0 | 软删除:0=正常,1=已删除 | + +## 核心表结构 + +### 系统管理模块 + +#### 用户表 `tornado_user` + +| 字段名 | 类型 | 约束 | 说明 | +|:---|:---|:---|:---| +| id | INT | PK | 用户ID | +| username | VARCHAR(50) | NOT NULL, UNIQUE | 登录账号 | +| password | VARCHAR(200) | NOT NULL | 密码(bcrypt 哈希) | +| salt | VARCHAR(50) | NULLABLE | 密码盐值 | +| realname | VARCHAR(50) | NULLABLE | 真实姓名 | +| avatar | VARCHAR(500) | NULLABLE | 头像地址 | +| email | VARCHAR(100) | NULLABLE | 邮箱 | +| mobile | VARCHAR(20) | NULLABLE | 手机号 | +| gender | INT | DEFAULT 0 | 性别:0=未知 1=男 2=女 | +| dept_id | INT | NULLABLE, INDEX | 部门ID(外键) | +| position_id | INT | NULLABLE, INDEX | 岗位ID(外键) | +| level_id | INT | NULLABLE, INDEX | 职级ID(外键) | +| status | INT | DEFAULT 1 | 状态:1=正常 2=禁用 | +| create_user | VARCHAR(50) | NULLABLE | 通用字段 | +| create_time | DATETIME | DEFAULT NOW() | 通用字段 | +| update_user | VARCHAR(50) | NULLABLE | 通用字段 | +| update_time | DATETIME | DEFAULT NOW() | 通用字段 | +| is_delete | INT | DEFAULT 0, INDEX | 通用字段 | + +#### 角色表 `tornado_role` + +| 字段名 | 类型 | 约束 | 说明 | +|:---|:---|:---|:---| +| id | INT | PK | 角色ID | +| name | VARCHAR(100) | NOT NULL, INDEX | 角色名称 | +| code | VARCHAR(100) | NOT NULL, UNIQUE | 角色编码 | +| status | INT | DEFAULT 1 | 状态:1=正常 2=禁用 | +| sort | INT | DEFAULT 0 | 排序 | +| note | VARCHAR(255) | NULLABLE | 备注 | +| + 通用字段 | | | | + +#### 菜单表 `tornado_menu` + +| 字段名 | 类型 | 约束 | 说明 | +|:---|:---|:---|:---| +| id | INT | PK | 菜单ID | +| parent_id | INT | DEFAULT 0, INDEX | 父菜单ID(0=顶级) | +| name | VARCHAR(100) | NOT NULL | 菜单名称 | +| type | INT | NOT NULL | 类型:1=目录 2=菜单 3=按钮 | +| path | VARCHAR(200) | NULLABLE | 路由路径 | +| component | VARCHAR(200) | NULLABLE | 前端组件路径 | +| permission | VARCHAR(200) | NULLABLE | 权限标识(如 sys:user:add) | +| icon | VARCHAR(100) | NULLABLE | 菜单图标 | +| sort | INT | DEFAULT 0 | 排序 | +| visible | INT | DEFAULT 1 | 是否可见:1=是 2=否 | +| status | INT | DEFAULT 1 | 状态:1=正常 2=禁用 | +| + 通用字段 | | | | + +#### 部门表 `tornado_dept` + +| 字段名 | 类型 | 约束 | 说明 | +|:---|:---|:---|:---| +| id | INT | PK | 部门ID | +| parent_id | INT | DEFAULT 0, INDEX | 父部门ID(0=顶级) | +| name | VARCHAR(100) | NOT NULL | 部门名称 | +| leader | VARCHAR(50) | NULLABLE | 负责人 | +| mobile | VARCHAR(20) | NULLABLE | 联系电话 | +| email | VARCHAR(100) | NULLABLE | 邮箱 | +| sort | INT | DEFAULT 0 | 排序 | +| status | INT | DEFAULT 1 | 状态:1=正常 2=禁用 | +| + 通用字段 | | | | + +#### 岗位表 `tornado_position` + +| 字段名 | 类型 | 约束 | 说明 | +|:---|:---|:---|:---| +| id | INT | PK | 岗位ID | +| name | VARCHAR(255) | NOT NULL, INDEX | 岗位名称 | +| status | INT | DEFAULT 0, INDEX | 状态:1=在用 2=停用 | +| sort | INT | DEFAULT 0 | 排序 | +| + 通用字段 | | | | + +#### 职级表 `tornado_level` + +| 字段名 | 类型 | 约束 | 说明 | +|:---|:---|:---|:---| +| id | INT | PK | 职级ID | +| name | VARCHAR(255) | NOT NULL, INDEX | 职级名称 | +| status | INT | DEFAULT 0, INDEX | 状态:1=在用 2=停用 | +| sort | INT | DEFAULT 0 | 排序 | +| + 通用字段 | | | | + +### 关联表 + +#### 用户角色关联表 `tornado_user_role` + +| 字段名 | 类型 | 约束 | 说明 | +|:---|:---|:---|:---| +| id | INT | PK | 主键 | +| user_id | INT | NOT NULL, INDEX | 用户ID | +| role_id | INT | NOT NULL, INDEX | 角色ID | +| + 通用字段 | | | | + +#### 角色菜单关联表 `tornado_role_menu` + +| 字段名 | 类型 | 约束 | 说明 | +|:---|:---|:---|:---| +| id | INT | PK | 主键 | +| role_id | INT | NOT NULL, INDEX | 角色ID | +| menu_id | INT | NOT NULL, INDEX | 菜单ID | +| + 通用字段 | | | | + +### 数据字典模块 + +#### 字典表 `tornado_dict` + +| 字段名 | 类型 | 约束 | 说明 | +|:---|:---|:---|:---| +| id | INT | PK | 字典ID | +| name | VARCHAR(200) | NOT NULL | 字典名称 | +| code | VARCHAR(200) | NOT NULL, UNIQUE | 字典编码 | +| status | INT | DEFAULT 1 | 状态 | +| note | VARCHAR(255) | NULLABLE | 备注 | +| + 通用字段 | | | | + +#### 字典项表 `tornado_dict_item` + +| 字段名 | 类型 | 约束 | 说明 | +|:---|:---|:---|:---| +| id | INT | PK | 字典项ID | +| dict_code | VARCHAR(200) | NOT NULL, INDEX | 所属字典编码 | +| label | VARCHAR(200) | NOT NULL | 显示名 | +| value | VARCHAR(200) | NOT NULL | 字典值 | +| sort | INT | DEFAULT 0 | 排序 | +| status | INT | DEFAULT 1 | 状态 | +| note | VARCHAR(255) | NULLABLE | 备注 | +| + 通用字段 | | | | + +### 内容管理模块 + +#### 文章表 `tornado_article` + +| 字段名 | 类型 | 约束 | 说明 | +|:---|:---|:---|:---| +| id | INT | PK | 文章ID | +| category_id | INT | NULLABLE, INDEX | 分类ID | +| title | VARCHAR(200) | NOT NULL | 文章标题 | +| cover | VARCHAR(500) | NULLABLE | 封面图片 | +| summary | VARCHAR(500) | NULLABLE | 摘要 | +| content | TEXT | NULLABLE | 正文(富文本) | +| author | VARCHAR(50) | NULLABLE | 作者 | +| source | VARCHAR(200) | NULLABLE | 来源 | +| view_count | INT | DEFAULT 0 | 浏览次数 | +| is_top | INT | DEFAULT 0 | 是否置顶 | +| status | INT | DEFAULT 1 | 状态 | +| sort | INT | DEFAULT 0 | 排序 | +| + 通用字段 | | | | + +#### 文章分类表 `tornado_category` + +| 字段名 | 类型 | 约束 | 说明 | +|:---|:---|:---|:---| +| id | INT | PK | 分类ID | +| parent_id | INT | DEFAULT 0, INDEX | 父分类ID | +| name | VARCHAR(100) | NOT NULL | 分类名称 | +| sort | INT | DEFAULT 0 | 排序 | +| status | INT | DEFAULT 1 | 状态 | +| + 通用字段 | | | | + +#### 通知公告表 `tornado_notice` + +| 字段名 | 类型 | 约束 | 说明 | +|:---|:---|:---|:---| +| id | INT | PK | 通知ID | +| title | VARCHAR(200) | NOT NULL | 标题 | +| content | TEXT | NULLABLE | 内容 | +| type | INT | DEFAULT 1 | 类型:1=通知 2=公告 | +| status | INT | DEFAULT 1 | 状态 | +| + 通用字段 | | | | + +### 日志模块 + +#### 登录日志表 `tornado_login_log` + +| 字段名 | 类型 | 约束 | 说明 | +|:---|:---|:---|:---| +| id | INT | PK | 日志ID | +| username | VARCHAR(50) | NULLABLE | 登录账号 | +| ip | VARCHAR(50) | NULLABLE | 登录IP | +| location | VARCHAR(200) | NULLABLE | 登录地点 | +| browser | VARCHAR(100) | NULLABLE | 浏览器 | +| os | VARCHAR(100) | NULLABLE | 操作系统 | +| status | INT | DEFAULT 1 | 状态:1=成功 2=失败 | +| msg | VARCHAR(500) | NULLABLE | 提示消息 | +| + 通用字段 | | | | + +#### 操作日志表 `tornado_operation_log` + +| 字段名 | 类型 | 约束 | 说明 | +|:---|:---|:---|:---| +| id | INT | PK | 日志ID | +| username | VARCHAR(50) | NULLABLE | 操作人 | +| module | VARCHAR(100) | NULLABLE | 操作模块 | +| action | VARCHAR(100) | NULLABLE | 操作动作 | +| method | VARCHAR(10) | NULLABLE | 请求方法 | +| url | VARCHAR(500) | NULLABLE | 请求URL | +| params | TEXT | NULLABLE | 请求参数 | +| ip | VARCHAR(50) | NULLABLE | 操作IP | +| status | INT | DEFAULT 1 | 状态 | +| duration | INT | NULLABLE | 耗时(毫秒) | +| + 通用字段 | | | | + +### 定时任务模块 + +#### 任务表 `tornado_job` + +| 字段名 | 类型 | 约束 | 说明 | +|:---|:---|:---|:---| +| id | INT | PK | 任务ID | +| name | VARCHAR(200) | NOT NULL | 任务名称 | +| job_group | VARCHAR(100) | NULLABLE | 任务分组 | +| invoke_target | VARCHAR(500) | NOT NULL | 调用目标 | +| cron_expression | VARCHAR(200) | NOT NULL | Cron 表达式 | +| misfire_policy | INT | DEFAULT 3 | 执行策略 | +| concurrent | INT | DEFAULT 0 | 是否并发 | +| status | INT | DEFAULT 1 | 状态:1=运行 2=暂停 | +| note | VARCHAR(255) | NULLABLE | 备注 | +| + 通用字段 | | | | + +#### 任务日志表 `tornado_job_log` + +| 字段名 | 类型 | 约束 | 说明 | +|:---|:---|:---|:---| +| id | INT | PK | 日志ID | +| job_id | INT | NOT NULL, INDEX | 任务ID | +| job_name | VARCHAR(200) | NULLABLE | 任务名称 | +| invoke_target | VARCHAR(500) | NULLABLE | 调用目标 | +| job_message | VARCHAR(500) | NULLABLE | 执行结果 | +| status | INT | DEFAULT 1 | 状态 | +| exception_info | TEXT | NULLABLE | 异常信息 | +| start_time | DATETIME | NULLABLE | 开始时间 | +| end_time | DATETIME | NULLABLE | 结束时间 | +| + 通用字段 | | | | + +## ER 关系图 + +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ tornado │ │ tornado │ │ tornado │ +│ _user │ │ _user_role │ │ _role │ +│──────────────│ │──────────────│ │──────────────│ +│ id (PK) │←───│ user_id (FK) │ │ id (PK) │ +│ username │ │ role_id (FK) │───→│ name │ +│ password │ └──────────────┘ │ code │ +│ realname │ └──────┬───────┘ +│ dept_id (FK) │───→┌──────────────┐ │ +│ position_id │ │ tornado │ │ +│ (FK) │ │ _dept │ │ +│ level_id (FK)│ │──────────────│ │ +│ │ │ id (PK) │ │ +│ │ │ parent_id │ │ +│ │ │ name │ │ +│ │ └──────────────┘ │ +│ │ │ +│ │ ┌──────────────┐ │ +│ │ │ tornado │ │ +│ │ │ _role_menu │ │ +│ │ │──────────────│ │ +│ │ │ role_id (FK) │←───────────┘ +│ │ │ menu_id (FK) │───→┌──────────────┐ +│ │ └──────────────┘ │ tornado │ +└──────────────┘ │ _menu │ + │──────────────│ +┌──────────────┐ │ id (PK) │ +│ tornado │ │ parent_id │ +│ _position │ │ name │ +│──────────────│ │ permission │ +│ id (PK) │ │ type │ +│ name │ └──────────────┘ +└──────────────┘ + +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ tornado │ │ tornado │ │ tornado │ +│ _dict │ │ _dict_item │ │ _config │ +│──────────────│ │──────────────│ │──────────────│ +│ id (PK) │←───│ dict_code │ │ id (PK) │ +│ name │ │ label │ │ name │ +│ code │ │ value │ │ code │ +└──────────────┘ └──────────────┘ └──────────────┘ + +┌──────────────┐ ┌──────────────┐ +│ tornado │ │ tornado │ +│ _article │ │ _category │ +│──────────────│ │──────────────│ +│ id (PK) │ │ id (PK) │ +│ category_id │───→│ parent_id │ +│ title │ │ name │ +│ content │ └──────────────┘ +└──────────────┘ +``` + +## 命名约定 + +| 约定项 | 规范 | 示例 | +|:---|:---|:---| +| 表名 | `{DB_PREFIX}{module_name}` | `tornado_position` | +| 字段名 | 小写下划线(snake_case) | `create_user`、`dict_code` | +| 主键 | `id`,INT,AUTO_INCREMENT | — | +| 外键 | `{关联表名}_id` | `user_id`、`dept_id` | +| 索引 | 外键字段、高频查询字段 | `status`、`username` | +| 布尔状态 | INT 类型,0/1 或 1/2 | `is_delete: 0/1`、`status: 1/2` | +| 时间字段 | DATETIME 类型 | `create_time`、`update_time` | + +:::warning 外键约束 +不使用数据库层面的外键约束(FOREIGN KEY),而是通过应用层代码维护引用完整性。这样做的好处是:简化跨库迁移、避免级联删除风险、提升写入性能。删除关联数据时,由 Service 层的 `_before_delete` 钩子检查引用关系。 +::: + +## 多数据库驱动支持 + +通过 `DB_DRIVER` 环境变量切换数据库驱动,`src/core/database.py` 的 `build_engine()` 函数按驱动构建方言感知的 SQLAlchemy 引擎: + +| 驱动 | DB_DRIVER | 连接 URL 格式 | 特殊处理 | +|:---|:---|:---|:---| +| MySQL | `mysql`(默认) | `mysql+pymysql://user:pass@host:3306/db` | `CLIENT.FOUND_ROWS` 避免重复提交时 rowcount=0 误判 | +| PostgreSQL | `postgresql` | `postgresql+psycopg://user:pass@host:5432/db` | `options="-c timezone=UTC"` 确保 naive UTC 一致 | +| SQL Server | `mssql` | `mssql+pymssql://user:pass@host:1433/db` | `login_timeout` 连接超时 | +| SQLite | `sqlite` | `sqlite:///./db.sqlite3` | `check_same_thread=False`,跳过连接池参数 | +| Oracle | `oracle` | `oracle+oracledb://user:pass@host:1521/db` | thin 模式,UTF-8 编码 | + +```python +# src/core/database.py - 引擎构建核心逻辑 +# ============================================================ +# 引擎构建 +# ============================================================ +def build_engine(driver: str | None = None, database_url: str | None = None): + """按 DB_DRIVER 构建方言感知的 SQLAlchemy 引擎 + + - mysql: connect_timeout + client_flag FOUND_ROWS(UPDATE rowcount=匹配行数, + 避免重复提交相同内容时 rowcount=0 被误判"更新失败");pymysql 懒加载 + - postgresql: connect_timeout + options="-c timezone=UTC"(aware-UTC 默认值落库为 + naive UTC,与 base_model.py 的 naive-UTC 读取约定一致,防时区偏移) + - mssql: login_timeout(pymssql 连接超时参数) + - oracle: oracledb 懒加载(thin 模式,UTF-8);connect_args 传 encoding/nencoding + - sqlite: 仅 check_same_thread=False,跳过连接池容量参数(QueuePool 默认即可) + """ + from core.config import ( + SQLALCHEMY_DATABASE_URL, DB_DRIVER, TORNADO_DEBUG, + DB_POOL_SIZE, DB_MAX_OVERFLOW, DB_POOL_RECYCLE, + DB_POOL_TIMEOUT, DB_CONNECT_TIMEOUT, + ) + from core.config.database import _normalize_driver + + drv = _normalize_driver(driver or DB_DRIVER) + url = database_url or SQLALCHEMY_DATABASE_URL + common = {"pool_pre_ping": True, "echo": TORNADO_DEBUG, "echo_pool": TORNADO_DEBUG} + + if drv == 'mysql': + # 懒加载:仅 mysql 分支需要 pymysql,sqlite/pg/mssql 无需安装 + from pymysql.constants import CLIENT + return create_engine( + url, + pool_size=DB_POOL_SIZE, + pool_recycle=DB_POOL_RECYCLE, + pool_timeout=DB_POOL_TIMEOUT, + max_overflow=DB_MAX_OVERFLOW, + pool_use_lifo=True, + connect_args={ + "connect_timeout": DB_CONNECT_TIMEOUT, + "client_flag": CLIENT.FOUND_ROWS, + }, + **common, + ) + + if drv == 'postgresql': + return create_engine( + url, + pool_size=DB_POOL_SIZE, + pool_recycle=DB_POOL_RECYCLE, + pool_timeout=DB_POOL_TIMEOUT, + max_overflow=DB_MAX_OVERFLOW, + pool_use_lifo=True, + connect_args={ + "connect_timeout": DB_CONNECT_TIMEOUT, + "options": "-c timezone=UTC", + }, + **common, + ) + + if drv == 'mssql': + return create_engine( + url, + pool_size=DB_POOL_SIZE, + pool_recycle=DB_POOL_RECYCLE, + pool_timeout=DB_POOL_TIMEOUT, + max_overflow=DB_MAX_OVERFLOW, + pool_use_lifo=True, + connect_args={"login_timeout": DB_CONNECT_TIMEOUT}, + **common, + ) + + if drv == 'oracle': + # 懒加载:仅 oracle 分支需要 oracledb(thin 模式,无需 Oracle Instant Client) + import oracledb # noqa: F401 + return create_engine( + url, + pool_size=DB_POOL_SIZE, + pool_recycle=DB_POOL_RECYCLE, + pool_timeout=DB_POOL_TIMEOUT, + max_overflow=DB_MAX_OVERFLOW, + pool_use_lifo=True, + connect_args={"encoding": "UTF-8", "nencoding": "UTF-8"}, + **common, + ) + + # sqlite + return create_engine(url, connect_args={"check_same_thread": False}, **common) +``` + +业务代码无需修改,SQLAlchemy 自动处理方言差异(如分页语法、自增策略等)。 + +### 会话管理 + +```python +# src/core/database.py +from sqlalchemy.orm import scoped_session, sessionmaker + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +db = scoped_session(SessionLocal) +``` + +`scoped_session` 基于 thread-local 实现线程级隔离,每个线程持有独立的数据库会话。Repository 层通过 `self.db` 使用会话,写操作内部自动 `db.commit()`,Service 层 `try/finally` 确保 `repo.close()` 释放会话。 + try: + independent_session.add(obj) + independent_session.commit() + except Exception: + independent_session.rollback() + raise + finally: + independent_session.close() +``` + +:::tip 约束命名约定 +`database.py` 中定义了自定义约束命名约定,索引前缀使用 `idx_` 而非默认的 `ix_`: +```python +convention = { + "ix": "idx_%(table_name)s_%(column_0_name)s", + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s", +} +metadata = MetaData(naming_convention=convention) +Base = declarative_base(metadata=metadata) +``` +::: + +## 总结 + +数据库设计以「通用字段 + 软删除 + 应用层外键」为核心策略。所有业务表继承 `BaseModel` 获得统一的通用字段,通过 `is_delete` 实现软删除,通过应用层代码维护引用完整性。核心表覆盖系统管理(用户/角色/菜单/部门/岗位/职级)、数据字典、内容管理(文章/分类/通知)、日志管理、定时任务等功能域。多数据库驱动通过 `src/core/database.py` 按方言构建引擎,业务代码与具体数据库解耦。会话通过 `scoped_session` 实现线程级隔离。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume3/directory.md b/DjangoAdmin_Tornado_EleVue/zh/volume3/directory.md new file mode 100644 index 0000000..9633bd5 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume3/directory.md @@ -0,0 +1,255 @@ +# 目录结构设计 + +本章详细说明项目目录结构设计,包括后端 `src/` 和前端 `ui/src/` 的完整目录树及各目录的设计意图。 + +:::tip 设计原则 +目录结构遵循「按功能分组、按职责分层」的原则。后端 `src/` 按技术层次组织(config / core / common / api / modules),前端 `ui/src/` 按功能领域组织(api / views / components / store)。每个目录有明确的边界和单一职责。 +::: + +## 后端目录结构 + +``` +src/ +├── app.py // 应用工厂 +├── bootstrap.py // 启动引导 +│ +├── config/ // 配置模块 +│ ├── __init__.py +│ ├── app.py // 应用基础配置 +│ ├── auth.py // JWT 与登录安全配置 +│ ├── captcha.py // 验证码配置 +│ ├── database.py // 数据库配置 +│ ├── email.py // 邮件配置 +│ ├── logging.py // 日志配置 +│ └── redis.py // Redis 配置 +│ +├── core/ // 核心基础设施 +│ ├── __init__.py +│ ├── base_handler.py // 请求处理器基类 +│ ├── base_model.py // ORM 模型基类 +│ ├── base_repository.py // 通用 Repository 基类 +│ ├── base_service.py // 通用 Service 基类 +│ ├── base_schema.py // 通用 Schema 基类 +│ ├── context.py // 上下文变量 +│ ├── database.py // 数据库引擎与会话管理 +│ ├── exceptions.py // 异常体系 +│ ├── jwt.py // JWT 令牌处理 +│ ├── logger.py // 日志配置 +│ ├── metrics.py // 性能指标 +│ ├── password.py // 密码加密工具 +│ ├── redis.py // Redis 客户端 +│ ├── response.py // 统一响应封装 +│ └── tracer.py // 链路追踪 +│ +├── common/ // 公共模块 +│ ├── __init__.py +│ ├── permission.py // 权限解析 +│ ├── middleware/ // 中间件 +│ │ ├── __init__.py +│ │ ├── access_decorators.py // 访问控制装饰器 +│ │ ├── authentication.py // 认证中间件 +│ │ ├── cors.py // 跨域处理 +│ │ ├── login_log.py // 登录日志装饰器 +│ │ ├── metrics.py // 请求指标中间件 +│ │ ├── operation_log.py // 操作日志装饰器 +│ │ └── trace_id.py // 链路追踪中间件 +│ └── utils/ // 工具函数 +│ ├── __init__.py +│ ├── captcha.py // 验证码工具 +│ ├── crypto.py // 密码加密工具 +│ ├── dict_util.py // 数据字典工具 +│ ├── file.py // 文件工具 +│ ├── helpers.py // 通用工具函数 +│ ├── ip2region.py // IP 地理位置解析 +│ ├── rich_text.py // 富文本处理 +│ └── validators.py // 校验工具 +│ +├── modules/ // 业务模块 +│ ├── __init__.py +│ ├── auth/ // 认证模块 +│ ├── user/ // 用户管理 +│ ├── role/ // 角色管理 +│ ├── menu/ // 菜单管理 +│ ├── dept/ // 部门管理 +│ ├── position/ // 岗位管理 +│ ├── level/ // 职级管理 +│ ├── dict/ // 字典管理 +│ ├── dict_item/ // 字典项管理 +│ ├── config/ // 系统配置 +│ ├── config_item/ // 配置项管理 +│ ├── notice/ // 通知公告 +│ ├── link/ // 友情链接 +│ ├── category/ // 分类管理 +│ ├── article/ // 文章管理 +│ ├── param/ // 参数管理 +│ ├── city/ // 城市级联 +│ ├── file_template/ // 文件模板 +│ ├── job/ // 定时任务 +│ ├── job_log/ // 任务日志 +│ ├── upload/ // 文件上传 +│ ├── login_log/ // 登录日志 +│ ├── operation_log/ // 操作日志 +│ ├── user_role/ // 用户角色关联 +│ ├── role_menu/ // 角色菜单关联 +│ ├── example/ // 示例模块 +│ └── generator/ // 代码生成器 +│ +├── api/ // API 路由定义 +│ ├── __init__.py +│ └── v1/ // v1 版本路由 +│ ├── __init__.py +│ ├── router.py // 路由聚合 +│ ├── auth.py // 认证路由 +│ ├── index.py // 首页路由 +│ ├── upload.py // 上传路由 +│ ├── user.py // 用户路由 +│ ├── role.py // 角色路由 +│ ├── menu.py // 菜单路由 +│ ├── dept.py // 部门路由 +│ ├── position.py // 岗位路由 +│ ├── level.py // 职级路由 +│ ├── dict.py // 字典路由 +│ ├── dict_item.py // 字典项路由 +│ ├── config.py // 配置路由 +│ ├── config_item.py // 配置项路由 +│ ├── city.py // 城市路由 +│ ├── notice.py // 公告路由 +│ ├── link.py // 友链路由 +│ ├── category.py // 分类路由 +│ ├── article.py // 文章路由 +│ ├── param.py // 参数路由 +│ ├── file_template.py // 文件模板路由 +│ ├── job.py // 定时任务路由 +│ ├── job_log.py // 任务日志路由 +│ ├── generator.py // 代码生成器路由 +│ ├── operation_log.py // 操作日志路由 +│ ├── login_log.py // 登录日志路由 +│ └── example.py // 示例路由 +│ +├── extensions/ // 扩展注册 +│ ├── __init__.py // 扩展初始化 +│ ├── sqlalchemy.py // 数据库扩展 +│ ├── redis.py // Redis 扩展 +│ ├── swagger.py // Swagger 占位 +│ └── sentry.py // Sentry 占位 +│ +└── constants/ // 常量定义 + └── __init__.py // 常量定义 +``` + +### 目录设计意图 + +| 目录 | 设计意图 | +|:---|:---| +| `config/` | 配置按功能拆分子模块,统一从 `.env` 加载,避免单一大文件 | +| `core/` | 基础设施层,提供所有模块共用的基类、工具和配置。不包含业务逻辑 | +| `common/middleware/` | 中间件独立于 core,职责单一,便于按需注册和测试 | +| `common/utils/` | 工具函数按功能拆分,提供密码、文件、缓存等通用能力 | +| `api/v1/` | 路由装配层,将所有模块的 Handler 集中注册,便于查看全量 API | +| `modules/` | 业务模块层,每个模块包含 handlers + schemas + service + repository + models | +| `extensions/` | 扩展注册,导出 SQLAlchemy/Redis 等单例,供全项目使用 | +| `constants/` | 常量独立管理,避免硬编码散落各处 | + +## 前端目录结构 + +``` +ui/src/ +├── main.ts // 入口文件 +├── App.vue // 根组件 +├── api/ // API 接口定义 +│ ├── system/ // 系统管理接口 +│ ├── content/ // 内容管理接口 +│ ├── data/ // 数据管理接口 +│ ├── monitor/ // 监控管理接口 +│ ├── tool/ // 工具接口 +│ ├── file/ // 文件接口 +│ ├── region/ // 地区接口 +│ ├── setting/ // 设置接口 +│ ├── dashboard/ // 仪表盘接口 +│ └── common/ // 公共接口 +├── views/ // 页面视图 +│ ├── login/ // 登录页 +│ ├── dashboard/ // 仪表盘 +│ ├── system/ // 系统管理页面 +│ ├── content/ // 内容管理页面 +│ ├── data/ // 数据管理页面 +│ ├── monitor/ // 监控管理页面 +│ ├── file/ // 文件管理页面 +│ ├── tool/ // 工具页面 +│ ├── setting/ // 设置页面 +│ ├── exception/ // 异常页面 +│ └── about/ // 关于页面 +├── components/ // 公共组件 +│ ├── Table/ // 表格组件 +│ ├── Form/ // 表单组件 +│ ├── Modal/ // 弹窗组件 +│ ├── Upload/ // 上传组件 +│ ├── Editor/ // 富文本编辑器 +│ ├── Excel/ // Excel 导入导出 +│ ├── Select/ // 选择器组件 +│ ├── Cropper/ // 图片裁剪 +│ ├── Qrcode/ // 二维码 +│ ├── Page/ // 分页组件 +│ ├── Password/ // 密码组件 +│ ├── Authority/ // 权限组件 +│ ├── ChinaArea/ // 中国行政区划 +│ ├── Lockscreen/ // 锁屏 +│ ├── CountTo/ // 数字动画 +│ ├── Render/ // 自定义渲染 +│ ├── Region/ // 区域选择 +│ ├── TableSelect/ // 表格选择 +│ ├── icon/ // 图标组件 +│ ├── importFile/ // 文件导入 +│ ├── numberInput/ // 数字输入 +│ └── pagination/ // 分页组件 +├── store/ // 状态管理 +│ ├── index.ts // Store 入口 +│ └── modules/ // 状态模块 +├── router/ // 路由配置 +│ ├── index.ts // 路由实例 +│ └── menus/ // 菜单路由 +├── hooks/ // 组合式函数 +│ ├── core/ // 核心 hooks +│ ├── event/ // 事件 hooks +│ ├── setting/ // 设置 hooks +│ └── web/ // Web hooks +├── layout/ // 布局组件 +│ └── components/ // 布局子组件 +├── directives/ // 自定义指令 +├── enums/ // 前端枚举 +├── plugins/ // 插件注册 +├── settings/ // 应用设置 +├── styles/ // 全局样式 +├── utils/ // 工具函数 +│ ├── http/ // Axios 封装 +│ ├── auth.ts // Token 管理 +│ ├── dateUtil.ts // 日期格式化 +│ ├── downloadFile.ts // 文件下载 +│ ├── validate.ts // 表单校验 +│ └── Storage.ts // 本地存储 +└── assets/ // 静态资源 + ├── icons/ // 图标 + └── images/ // 图片 +``` + +### 前后端目录对应关系 + +| 前端目录 | 后端目录 | 说明 | +|:---|:---|:---| +| `api/system/position.ts` | `api/v1/position.py` | API 接口定义一一对应 | +| `views/system/position/` | `modules/position/` | 页面视图对应业务模块 | +| `store/modules/user.ts` | `modules/auth/service.py` | 用户状态对应认证服务 | +| `components/` | `common/utils/` + `core/` | 公共组件对应后端工具 | + +## 目录规范 + +:::warning 目录命名规范 +1. **后端**:目录名使用小写下划线(snake_case),与 Python 模块命名一致 +2. **前端**:目录名使用小写 kebab-case 或 camelCase,与 Vue 生态习惯一致 +3. **模块文件**:每个模块固定包含 `handlers.py` / `schemas.py` / `service.py` / `repository.py` / `models.py` 五个文件 +4. **路由文件**:每个模块在 `api/v1/` 下对应一个路由文件,导出 `routes` 列表 +::: + +## 总结 + +目录结构是项目可维护性的基础。后端按技术层次(config / core / common / api / modules)组织,前端按功能领域(api / views / components / store)组织。每个目录有明确的边界和单一职责,模块内部遵循固定的文件结构(handlers + schemas + service + repository + models)。这种设计使得新开发者能快速定位代码,新模块能按既定模式快速创建。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume3/extensibility.md b/DjangoAdmin_Tornado_EleVue/zh/volume3/extensibility.md new file mode 100644 index 0000000..68616a0 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume3/extensibility.md @@ -0,0 +1,539 @@ +# 扩展性设计 + +本章详细描述系统的扩展性设计,包括模块可插拔、数据库驱动可切换、中间件可插拔三大扩展维度。 + +:::tip 扩展性设计原则 +遵循开闭原则(OCP):对扩展开放,对修改关闭。新功能通过添加新模块实现,而非修改现有代码。核心框架通过基类、装饰器、中间件等机制提供扩展点,业务模块只需声明差异即可接入。 +::: + +## 一、模块可插拔设计 + +### 模块标准结构 + +每个业务模块遵循固定的四文件结构,新增模块只需创建这四个文件即可接入系统: + +``` +src/modules/{group}/{name}/ +├── models.py # ORM 模型 +├── repository.py # 数据访问层 +├── schemas.py # 请求校验 +└── service.py # 业务逻辑层 +``` + +### 新增模块步骤 + +以新增「培训管理(Training)」模块为例。 + +**步骤一:创建模块目录和文件** + +创建 `src/modules/training/` 目录,包含以下四个文件: + +`models.py` — ORM 模型: + +```python +# src/modules/training/models.py +from sqlalchemy import Column, String, Integer, text +from core.base_model import BaseModel +from core.base_model import base_model +from core.config import DB_PREFIX + + +class Training(BaseModel): + __tablename__ = DB_PREFIX + "training" + __table_comment__ = "培训记录表" + + title = Column(String(200), nullable=False, comment="培训标题") + content = Column(String(2000), nullable=True, comment="培训内容") + trainer = Column(String(50), nullable=True, comment="培训讲师") + status = Column(Integer, default=1, server_default=text('1'), comment="状态") + sort = Column(Integer, default=0, server_default=text('0'), comment="排序") +``` + +`repository.py` — 数据访问层: + +```python +# src/modules/training/repository.py +from training.models import Training +from core.base_repository import BaseRepository + + +class TrainingRepository(BaseRepository(Training]): + pass + + +training_repo = TrainingRepository(Training) +``` + +`schemas.py` — 请求校验: + +```python +# src/modules/training/schemas.py +from pydantic import BaseModel, Field +from core.base_schema import BaseSchema + + +class TrainingForm(BaseSchema): + title: str = Field(..., min_length=1, max_length=200, description="培训标题") + content: str = Field(None, max_length=2000, description="培训内容") + trainer: str = Field(None, max_length=50, description="培训讲师") + status: int = Field(..., ge=1, le=2, description="状态") + sort: int = Field(..., ge=0, le=99999, description="排序") + + +class TrainingStatusForm(BaseModel): + id: int = Field(..., gt=0, description="培训ID") + status: int = Field(..., ge=1, le=2, description="状态") +``` + +`service.py` — 业务逻辑层: + +```python +# src/modules/training/service.py +from core.base_service import BaseService +from training.models import Training +from training.repository import training_repo + + +class TrainingService(BaseService(Training]): + repo = training_repo + model = Training + page_like_fields = ('title', 'trainer') + page_eq_fields = ('status',) + page_order_by = (('sort', 'asc'),) + unique_fields = {'title': '培训标题不能重复'} + + +training_service = TrainingService() +``` + +**步骤二:创建 Handler** + +创建 `src/modules/training/handlers.py`: + +```python +# src/modules/training/handlers.py +from common.middleware.access_decorators import check_demo, permission_required +from common.middleware.operation_log import operation_log +from core import response as R +from core.base_handler import BaseHandler +from modules.training.service import training_service + + +class TrainingPageHandler(BaseHandler): + @permission_required("sys:training:page") + async def get(self): + return await training_service.get_page(self) + + +class TrainingDetailHandler(BaseHandler): + @permission_required("sys:training:detail") + async def get(self, id): + return R.ok(self, data=training_service.get_detail(id)) + + +class TrainingAddHandler(BaseHandler): + @permission_required("sys:training:add") + @check_demo + @operation_log("培训管理", "添加") + async def post(self): + return await training_service.add(self) + + +class TrainingUpdateHandler(BaseHandler): + @permission_required("sys:training:update") + @check_demo + @operation_log("培训管理", "更新") + async def put(self): + return await training_service.update(self) + + +class TrainingDeleteHandler(BaseHandler): + @permission_required("sys:training:delete") + @check_demo + @operation_log("培训管理", "删除") + async def delete(self, id): + return await training_service.delete(self, id) + + +class TrainingStatusHandler(BaseHandler): + @permission_required("sys:training:status") + @check_demo + @operation_log("培训管理", "设置状态") + async def put(self): + return await training_service.update_status(self) + + +class TrainingBatchDeleteHandler(BaseHandler): + @permission_required("sys:training:delete") + @check_demo + @operation_log("培训管理", "批量删除") + async def post(self): + return await training_service.batch_delete(self) +``` + +**步骤三:注册路由** + +在 `src/api/v1/` 下创建路由文件,并在 `router.py` 中注册: + +```python +# src/api/v1/training.py +from modules.training.handlers import ( + TrainingAddHandler, TrainingBatchDeleteHandler, TrainingDeleteHandler, + TrainingDetailHandler, TrainingPageHandler, TrainingStatusHandler, + TrainingUpdateHandler, +) + +routes = [ + (r"/training/page", TrainingPageHandler), + (r"/training/detail/(\d+)", TrainingDetailHandler), + (r"/training/add", TrainingAddHandler), + (r"/training/update", TrainingUpdateHandler), + (r"/training/delete/(\d+)", TrainingDeleteHandler), + (r"/training/status", TrainingStatusHandler), + (r"/training/batchDelete", TrainingBatchDeleteHandler), +] +``` + +```python +# src/api/v1/router.py +from api.v1 import training + +_RESOURCE_MODULES = [ + # ... 其他模块 ... + training, +] +``` + +**步骤四:配置菜单权限** + +在数据库 `tornado_menu` 表中插入对应的菜单和权限节点记录。权限标识格式为 `sys:{module}:{action}`,如 `sys:training:add`、`sys:training:page`。 + +### 模块解耦 + +模块之间通过以下方式解耦: + +| 解耦方式 | 说明 | 示例 | +|:---|:---|:---| +| **Repository 单例** | 模块级单例,通过 import 引用 | `from modules.user.repository import UserRepository` | +| **Service 单例** | 模块级单例,通过 import 引用 | `from user.service import user_service` | +| **跨模块引用** | 通过 Repository 引用其他模块的模型 | PositionService 引用 User 模型检查引用 | +| **事件机制** | 暂未实现(未来可通过事件总线解耦) | — | + +### 复杂模块扩展 + +对于业务逻辑复杂的模块(如 User、Role、Menu),在继承 `BaseService` 的基础上添加自定义方法: + +```python +# src/modules/user/service.py +class UserService(BaseService(User]): + repo = user_repo + model = User + page_like_fields = ('username', 'realname') + page_eq_fields = ('status', 'dept_id') + + # 自定义方法:用户详情(含角色列表) + def get_user_detail(self, user_id): + user = self.repo.get_by_id(user_id) + if not user: + return None + data = user.to_dict() + data['roleIds'] = get_user_role_ids(user_id) + data['roleNames'] = get_user_role_names(user_id) + return data + + # 自定义方法:修改密码 + def change_password(self, user_id, old_pwd, new_pwd): + user = self.repo.get_by_id(user_id) + if not verify_password(old_pwd, user.password): + return R.failed("原密码错误") + user.password = hash_password(new_pwd) + user_repo.create(user) + return R.ok(msg="密码修改成功") + + # 覆盖钩子:新增前哈希密码 + def _before_add(self, request, data): + data.password = hash_password(data.password or DEFAULT_PASSWORD) + + # 覆盖钩子:删除前检查 + def _before_delete(self, ids): + if 1 in parse_id_list(str(ids)): + return "超级管理员不能删除" + return None +``` + +## 二、数据库驱动可切换 + +### 驱动注册机制 + +项目通过 `_SUPPORTED_DRIVERS` 声明支持的驱动,`_normalize_driver()` 处理别名映射和校验,`build_database_url()` 按驱动构建连接串: + +```python +# src/config/database.py(简化) +import os +from urllib.parse import quote_plus + +# 支持的数据库驱动 +_SUPPORTED_DRIVERS = ("mysql", "postgresql", "mssql", "sqlite", "oracle") + + +def _normalize_driver(driver: str) -> str: + """归一化 DB_DRIVER:别名映射 + 校验""" + name = str(driver).strip().lower() + alias_map = { + 'postgres': 'postgresql', + 'pg': 'postgresql', + 'sqlserver': 'mssql', + 'sql_server': 'mssql', + 'sqlite3': 'sqlite', + } + name = alias_map.get(name, name) + if name not in _SUPPORTED_DRIVERS: + raise ValueError(f"不支持的 DB_DRIVER: {driver!r}") + return name + + +DB_DRIVER = _normalize_driver(os.getenv('DB_DRIVER', 'mysql')) +DB_HOST = os.getenv('DB_HOST', '127.0.0.1') +DB_PORT = int(os.getenv('DB_PORT', '3306')) +DB_DATABASE = os.getenv('DB_DATABASE', 'djangoadmin.tornado.elevue2') +DB_USERNAME = os.getenv('DB_USERNAME', 'root') +DB_PASSWORD = os.getenv('DB_PASSWORD', '') + + +def build_database_url(driver: str = None) -> str: + """按驱动构建 SQLAlchemy 连接串""" + drv = _normalize_driver(driver if driver is not None else DB_DRIVER) + + if drv == 'sqlite': + return 'sqlite:///./' + DB_DATABASE + + base = quote_plus(DB_USERNAME) + ':' + quote_plus(DB_PASSWORD) + '@' + DB_HOST + ':' + str(DB_PORT) + + if drv == 'mysql': + return 'mysql+pymysql://' + base + '/' + DB_DATABASE + '?charset=utf8mb4' + if drv == 'postgresql': + return 'postgresql+psycopg://' + base + '/' + DB_DATABASE + if drv == 'oracle': + return 'oracle+oracledb://' + base + '/?service_name=' + quote_plus(DB_DATABASE) + # mssql + return 'mssql+pymssql://' + base + '/' + DB_DATABASE + '?charset=utf8' +``` + +各驱动的连接串格式: + +| 驱动 | 连接串格式 | Python 包 | +|:----|:----|:----| +| mysql | `mysql+pymysql://user:pass@host:port/db?charset=utf8mb4` | PyMySQL | +| postgresql | `postgresql+psycopg://user:pass@host:port/db` | psycopg[binary] | +| mssql | `mssql+pymssql://user:pass@host:port/db?charset=utf8` | pymssql | +| oracle | `oracle+oracledb://user:pass@host:port/?service_name=db` | oracledb | +| sqlite | `sqlite:///./path/to/db` | 内置 | + +### 切换方式 + +只需修改 `.env` 文件中的 `DB_DRIVER` 和对应的连接参数: + +```bash +# MySQL(默认) +DB_DRIVER=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_NAME=djangoadmin +DB_USER=root +DB_PASSWORD=your_password + +# PostgreSQL +DB_DRIVER=postgresql +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_NAME=djangoadmin +DB_USER=postgres +DB_PASSWORD=your_password + +# SQLite +DB_DRIVER=sqlite +DB_DATABASE=./data.db + +# SQL Server +DB_DRIVER=mssql +DB_HOST=127.0.0.1 +DB_PORT=1433 +DB_NAME=djangoadmin +DB_USER=sa +DB_PASSWORD=your_password +``` + +:::warning 驱动切换注意事项 +1. 切换驱动后需安装对应的 Python 包(如 `psycopg[binary]` for PostgreSQL) +2. 数据迁移使用 `scripts/migrate_db.py` 工具 +3. SQLite 不支持并发写入,仅适用于开发/测试环境 +4. Oracle 需要额外的客户端库配置 +::: + +### 跨库数据迁移 + +```bash +# 干跑:核对迁移计划 +python scripts/migrate_db.py --src-driver mysql --src-host ... --dst-driver postgresql --dst-host ... --dry-run + +# 执行迁移 +python scripts/migrate_db.py --src-driver mysql --src-host ... --dst-driver postgresql --dst-host ... + +# 幂等重跑(先删除目标库数据) +python scripts/migrate_db.py --src-driver mysql --src-host ... --dst-driver postgresql --dst-host ... --drop-target-first +``` + +## 三、横切关注点扩展 + +### Handler 生命周期 + +Tornado 通过 Handler 生命周期方法实现横切关注点: + +```python +# src/core/base_handler.py +class BaseHandler(tornado.web.RequestHandler): + def set_default_headers(self): + """设置响应头:CORS + trace_id""" + cros_required(self) + apply_trace_id(self) + + @login_required + def prepare(self): + """请求预处理:JWT 认证""" + pass + + def on_finish(self): + """请求结束后:释放数据库会话""" + db.close() +``` + +### 自定义装饰器 + +通过 `permission_required` 和 `check_demo` 的模式,可以创建自定义装饰器: + +```python +# src/common/middleware/operation_log.py +def operation_log(module: str, action: str): + """操作日志记录装饰器""" + def decorator(func): + @wraps(func) + async def wrapper(self, *args, **kwargs): + start = time.time() + result = await func(self, *args, **kwargs) + duration = time.time() - start + save_log(module, action, duration) + return result + return wrapper + return decorator +``` + +使用示例: + +```python +class TrainingAddHandler(BaseHandler): + @permission_required("sys:training:add") + @check_demo + @operation_log("培训管理", "添加") + async def post(self): + return await training_service.add(self) +``` + +装饰器执行顺序(从外到内):`@permission_required` → `@check_demo` → `@operation_log` → 方法体。 + +## 四、装饰器扩展 + +### 自定义装饰器 + +通过 `permission_required` 和 `check_demo` 的模式,可以创建自定义装饰器: + +```python +# src/common/decorators.py + +# 自定义装饰器示例:操作日志记录 +def log_operation(module: str, action: str): + """ + 操作日志记录装饰器 + 自动记录接口调用的模块、操作类型、执行耗时 + + Args: + module: 所属模块名称,如 "培训管理" + action: 操作类型,如 "新增"、"编辑"、"删除" + """ + def decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + # 记录开始时间,用于计算接口执行耗时 + start = time.time() + # 执行原业务函数 + result = await func(*args, **kwargs) + # 计算耗时(秒) + duration = time.time() - start + # 异步记录操作日志到数据库(包含模块、操作、耗时等信息) + save_log(module, action, duration) + return result + return wrapper + return decorator + + +# src/modules/training/handlers.py + +# 使用示例:多个装饰器叠加使用 +# 执行顺序:从外到内(先 permission_required,再 check_demo,再 operation_log) +class TrainingAddHandler(BaseHandler): + @permission_required("sys:training:add") # 权限校验 + @check_demo # 演示环境拦截 + @operation_log("培训管理", "添加") # 操作日志记录 + async def post(self): + """新增培训记录""" + return await training_service.add(self) +``` + +## 五、配置扩展 + +### 环境变量驱动 + +所有可配置项通过 `.env` 文件管理,新增配置项只需: + +1. 在 `.env` 文件中添加变量 +2. 在 `src/config/` 对应模块中读取 +3. 在业务代码中引用 + +```bash +# .env +CUSTOM_FEATURE_ENABLED=true +CUSTOM_TIMEOUT=30 +``` + +```python +# src/config/custom.py +CUSTOM_FEATURE_ENABLED = os.getenv("CUSTOM_FEATURE_ENABLED", "false").lower() == "true" +CUSTOM_TIMEOUT = int(os.getenv("CUSTOM_TIMEOUT", "30")) +``` + +## 六、前端扩展 + +### 新增页面 + +1. 在 `ui/src/api/` 创建 API 文件 +2. 在 `ui/src/views/` 创建页面组件 +3. 在数据库 `tornado_menu` 表中配置菜单路由 + +### 新增组件 + +公共组件放在 `ui/src/components/` 目录,遵循 Vue3 组件规范。 + +## 扩展性矩阵 + +| 扩展维度 | 扩展方式 | 修改范围 | 复杂度 | +|:---|:---|:---|:---| +| 新增 CRUD 模块 | 创建 4 文件 + 注册路由 | 新增文件,不改现有代码 | 低 | +| 新增复杂模块 | 继承基类 + 自定义方法 | 新增文件,不改现有代码 | 中 | +| 切换数据库驱动 | 修改 .env 配置 | 仅改配置 | 低 | +| 新增中间件 | 创建中间件 + 注册 | 新增文件 + 改 app.py | 低 | +| 新增装饰器 | 创建装饰器 + 应用 | 新增文件 | 低 | +| 新增配置项 | .env + config 模块 | 新增配置 | 低 | +| 新增前端页面 | API + Views + Menu | 新增文件 + 改数据库 | 中 | + +## 总结 + +通过分层架构、基类模板方法、装饰器、中间件、环境变量配置等机制,实现了高度的可扩展性。新增 CRUD 模块只需 4 个标准文件 + 1 行路由注册,不修改任何现有代码。数据库驱动通过环境变量切换,中间件通过注册机制插拔。这种设计使得系统在功能持续增长的同时,保持代码结构的清晰和稳定。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume3/layer-arch.md b/DjangoAdmin_Tornado_EleVue/zh/volume3/layer-arch.md new file mode 100644 index 0000000..4f9c019 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume3/layer-arch.md @@ -0,0 +1,511 @@ +# 分层架构设计 + +本章详细描述系统的分层架构设计,包括各层的职责边界、依赖方向、通信方式,并以岗位(Position)模块为实例贯穿说明。 + +## 架构概览 + +:::tip 架构分层原则 +采用经典的分层架构,核心原则是:**依赖单向、职责单一、接口隔离**。上层只依赖下层接口,下层不感知上层存在。 +::: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ HTTP 层 (Handler) │ +│ src/modules/{module}/handlers.py │ +│ 职责:路由定义 · 权限装饰器 · 操作日志 · 响应返回 │ +│ 原则:不含业务逻辑,仅做请求转发 │ +└──────────────────────────┬──────────────────────────────────┘ + │ 调用 + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 业务层 (Service) │ +│ src/modules/{module}/service.py │ +│ 职责:业务逻辑 · 校验规则 · 字段组装 · 序列化加工 │ +│ 原则:不直接操作数据库,通过 Repository 访问数据 │ +└──────────────────────────┬──────────────────────────────────┘ + │ 调用 + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 数据层 (Repository) │ +│ src/modules/{module}/repository.py │ +│ 职责:数据库 CRUD · 查询条件组装 · 软删除过滤 │ +│ 原则:只做数据访问原语,不做业务校验和响应封装 │ +└──────────────────────────┬──────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Model 层 (ORM 模型) │ +│ src/modules/{module}/models.py │ +│ 职责:表结构定义 · 字段映射 · 序列化方法 │ +└──────────────────────────┬──────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 数据库 (MySQL / PostgreSQL / etc.) │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 各层详细职责 + +### 第一层:HTTP 层(Handler) + +**目录位置**:`src/modules/{module}/handlers.py` + +**核心职责**: + +1. **权限控制**:通过 `@permission_required` 装饰器声明所需权限 +2. **演示模式保护**:通过 `@check_demo` 装饰器阻止演示环境的写操作 +3. **操作日志**:通过 `@operation_log` 装饰器记录写操作日志 +4. **响应返回**:直接返回 Service 层的 `R` 对象 + +**设计原则**: + +- 不包含任何业务逻辑 +- 不直接访问数据库 +- 仅做请求转发和响应返回 +- 权限字符串遵循 `sys:{module}:{action}` 格式 + +**代码示例**(岗位模块): + +```python +# src/modules/position/handlers.py +# +====================================================================== +# | 模块: 岗位请求处理器层 +# | 说明: 承接岗位相关的 HTTP 请求,校验权限/演示模式并记录操作日志后, +# | 转交岗位服务层处理 +# +====================================================================== +"""岗位请求处理器。""" + +from common.middleware.access_decorators import check_demo, permission_required +from common.middleware.operation_log import operation_log +from core import response as R +from core.base_handler import BaseHandler +from modules.position.service import position_service + + +# ====================================================================== +# 查询岗位分页数据 +# ====================================================================== +class PositionPageHandler(BaseHandler): + """查询岗位分页数据。""" + + @permission_required("sys:position:list") + async def get(self): + """GET 请求:返回岗位分页列表。""" + return await position_service.get_page(self) + + +# ====================================================================== +# 查询岗位详情 +# ====================================================================== +class PositionDetailHandler(BaseHandler): + """查询岗位详情。""" + + @permission_required("sys:position:detail") + async def get(self, id): + """GET 请求:按 ID 返回岗位详情。 + + :param id: 岗位 ID(路径参数) + """ + return R.ok(self, data=position_service.get_detail(id)) + + +# ====================================================================== +# 添加岗位 +# ====================================================================== +class PositionAddHandler(BaseHandler): + """添加岗位。""" + + @permission_required("sys:position:add") + @check_demo + @operation_log("岗位管理", "添加") + async def post(self): + """POST 请求:新增岗位。""" + return await position_service.add(self) + + +# ====================================================================== +# 更新岗位 +# ====================================================================== +class PositionUpdateHandler(BaseHandler): + """更新岗位。""" + + @permission_required("sys:position:update") + @check_demo + @operation_log("岗位管理", "更新") + async def put(self): + """PUT 请求:更新岗位信息。""" + return await position_service.update(self) + + +# ====================================================================== +# 删除岗位 +# ====================================================================== +class PositionDeleteHandler(BaseHandler): + """删除岗位。""" + + @permission_required("sys:position:delete") + @check_demo + @operation_log("岗位管理", "删除") + async def delete(self, id): + """DELETE 请求:按 ID 删除岗位(软删除)。 + + :param id: 岗位 ID(路径参数) + """ + return await position_service.delete(self, id) + + +# ====================================================================== +# 设置岗位状态 +# ====================================================================== +class PositionStatusHandler(BaseHandler): + """设置岗位状态。""" + + @permission_required("sys:position:status") + @check_demo + @operation_log("岗位管理", "设置状态") + async def put(self): + """PUT 请求:修改岗位状态(如启用 / 禁用)。""" + return await position_service.update_status(self) + + +# ====================================================================== +# 获取岗位下拉列表 +# ====================================================================== +class PositionGetListHandler(BaseHandler): + """获取岗位下拉列表。""" + + async def get(self): + """GET 请求:返回岗位下拉列表(无需权限校验)。""" + return await position_service.get_options(self) + + +# ====================================================================== +# 批量删除岗位 +# ====================================================================== +class PositionBatchDeleteHandler(BaseHandler): + """批量删除岗位。""" + + @permission_required("sys:position:delete") + @check_demo + @operation_log("岗位管理", "批量删除") + async def post(self): + """POST 请求:批量删除岗位(软删除)。""" + return await position_service.batch_delete(self) +``` + +:::tip 装饰器顺序 +装饰器按从外到内执行:先 `@permission_required`(鉴权),再 `@check_demo`(演示模式),最后 `@operation_log`(操作日志)。所有 Handler 方法都是 `async def`,因为 Tornado Handler 方法默认异步。 +::: + +### 第二层:业务层(Service) + +**目录位置**:`src/modules/{module}/service.py` + +**核心职责**: + +1. **业务逻辑编排**:组合多个数据操作完成业务流程 +2. **唯一性校验**:通过 `unique_fields` 声明唯一约束 +3. **字段组装**:`_build_create_fields` / `_build_update_fields` 组装入库字段 +4. **序列化加工**:`_serialize` / `_serialize_detail` 加工响应数据 +5. **文件处理**:文件字段迁移、URL 补全 +6. **前置/后置钩子**:`_before_add` / `_before_update` / `_before_delete` 扩展点 + +**设计原则**: + +- 通过 Repository 访问数据库,不直接使用 `db.query()` +- 返回 `R` 对象,通过 `self.finish()` 写出 JSON +- 通过声明差异点定制通用 CRUD,减少重复代码 + +**代码示例**(岗位模块): + +```python +# src/modules/position/service.py +# +====================================================================== +# | 模块: 岗位业务逻辑层 +# | 说明: 定义岗位服务,声明差异点以复用通用 CRUD 流程,并导出服务单例 +# +====================================================================== +"""岗位业务逻辑。""" + +from core.base_service import BaseService +from modules.position.models import Position +from modules.position.repository import PositionRepository +from modules.position.schemas import PositionSchema, PositionStatusSchema + + +# ====================================================================== +# 岗位服务 +# ====================================================================== +class PositionService(BaseService): + """岗位服务。""" + + # ============================================================ + # 基础声明 + # ============================================================ + model = Position + repo_cls = PositionRepository + + # ============================================================ + # 差异点声明 + # ============================================================ + # 校验模型 + create_schema = PositionSchema + update_schema = PositionSchema + status_schema = PositionStatusSchema + # 分页查询字段 + page_like_fields = ("name",) + page_eq_fields = ("status",) + # 分页排序与下拉过滤 + page_order_by = (("sort", "asc"),) + options_filter = {"status": 1} + + +# ====================================================================== +# 服务单例 +# ====================================================================== +position_service = PositionService() +``` + +:::tip Service 层的两种模式 +简单 CRUD 模块继承 `BaseService`,只需声明差异点即可获得完整的增删改查能力。复杂模块(如用户、角色、菜单)在继承基础上添加自定义方法,或完全自定义 Service 函数。 +::: + +### 第三层:数据层(Repository) + +**目录位置**:`src/modules/{module}/repository.py` + +**核心职责**: + +1. **CRUD 操作**:`create` / `update` / `batch_delete` / `get_by_id` 等 +2. **查询构建**:`filter` / `filter_by` / `get_one` / `all` / `count` +3. **分页查询**:`paginate` 返回 `(items, total)` 元组 +4. **软删除过滤**:等值方法自动追加 `is_delete=0` +5. **数据清洗**:`_clean_model_data` 将 camelCase 转 snake_case + +**设计原则**: + +- 只做数据访问原语,不做 R 封装 +- 等值方法自动软删过滤,`filter()` 为原始出口 +- Service 层通过 `repo_cls` 关联 Repository 类,基类自动实例化 + +**代码示例**(岗位模块): + +```python +# src/modules/position/repository.py +# +====================================================================== +# | 模块: 岗位数据访问层 +# | 说明: 定义岗位仓储,继承 BaseRepository 获得通用 CRUD 与软删除能力 +# +====================================================================== +"""岗位数据访问。""" + +from core.base_repository import BaseRepository +from modules.position.models import Position + + +# ====================================================================== +# 岗位仓储 +# ====================================================================== +class PositionRepository(BaseRepository): + """岗位仓储。""" + + # ============================================================ + # 绑定的模型类 + # ============================================================ + model = Position +``` + +:::tip Repository 基类提供的方法 +`BaseRepository` 提供以下通用方法,子类可直接使用: + +- **查询**:`get_by_id` / `get_by_ids` / `get_one` / `all` / `count` / `exists` / `exists_by_field` +- **分页**:`paginate` 返回 `(items, total)` +- **写操作**:`create` / `update` / `batch_delete` +- **条件构建**:`filter`(原始出口) / `filter_by`(等值出口,自动软删过滤) +::: + +### 第四层:Model 层(ORM 模型) + +**目录位置**:`src/modules/{module}/models.py` + +**核心职责**: + +1. **表结构定义**:通过 SQLAlchemy Column 定义字段 +2. **通用字段继承**:`BaseModel` 提供 id/create_user/create_time/update_user/update_time/is_delete +3. **序列化方法**:`to_dict()` 自动排除敏感字段(password/salt/is_delete),驼峰命名 + +**代码示例**(岗位模块): + +```python +# src/modules/position/models.py +from sqlalchemy import Column, Integer, String, text + +from config.database import DB_PREFIX +from core.base_model import BaseModel + + +class Position(BaseModel): + """岗位模型。""" + __tablename__ = DB_PREFIX + "position" + __table_comment__ = "岗位表" + + # 岗位名称 + name = Column(String(255), nullable=False, index=True, comment="岗位名称") + # 岗位状态:1-在用 2-停用 + status = Column(Integer, default=0, server_default=text('0'), index=True, comment="岗位状态") + # 岗位排序 + sort = Column(Integer, default=0, server_default=text('0'), comment="岗位排序") +``` + +### Schema 层(请求校验) + +**目录位置**:`src/modules/{module}/schemas.py` + +**核心职责**: + +1. **请求参数校验**:使用 Pydantic v2 的 `BaseModel` + `Field` 声明约束 +2. **创建/编辑共用**:继承 `BaseSchema`(自带可选 id 字段) +3. **状态更新独立**:单独建类,只含 id + status + +**代码示例**(岗位模块): + +```python +# src/modules/position/schemas.py +from pydantic import BaseModel, Field + +from core.base_schema import BaseSchema + + +class PositionSchema(BaseSchema): + """岗位创建/编辑表单。""" + name: str = Field(..., min_length=1, max_length=150, description="岗位名称") + status: int = Field(..., ge=1, le=2, description="岗位状态:1-在用 2-停用") + sort: int = Field(..., ge=0, le=99999, description="岗位排序") + + +class PositionStatusSchema(BaseModel): + """岗位状态更新表单。""" + id: int = Field(..., gt=0, description="岗位ID") + status: int = Field(..., ge=1, le=2, description="岗位状态:1-在用 2-停用") +``` + +## 路由注册 + +**目录位置**:`src/api/v1/{module}.py` + +每个模块在 `src/api/v1/` 下定义路由文件,导出 `routes` 列表,由 `router.py` 统一聚合。 + +**代码示例**(岗位模块): + +```python +# src/api/v1/position.py +"""岗位相关路由。""" + +from modules.position.handlers import ( + PositionAddHandler, PositionBatchDeleteHandler, PositionDeleteHandler, + PositionDetailHandler, PositionGetListHandler, PositionPageHandler, + PositionStatusHandler, PositionUpdateHandler, +) + +routes = [ + (r"/position/page", PositionPageHandler), + (r"/position/detail/(\d+)", PositionDetailHandler), + (r"/position/getPositionList", PositionGetListHandler), + (r"/position/add", PositionAddHandler), + (r"/position/update", PositionUpdateHandler), + (r"/position/delete/(\d+)", PositionDeleteHandler), + (r"/position/status", PositionStatusHandler), + (r"/position/batchDelete", PositionBatchDeleteHandler), +] +``` + +`router.py` 中统一注册: + +```python +# src/api/v1/router.py +from api.v1 import position # 导入模块 + +_RESOURCE_MODULES = [ + # ... 其他模块 ... + position, # 注册 +] + +def build_routes() -> list: + routes = [] + for module in _RESOURCE_MODULES: + routes.extend([(V1_PREFIX + path, handler) for path, handler in module.routes]) + routes.append((V1_PREFIX + "/health", HealthHandler)) + return routes +``` + +## 依赖方向 + +``` +HTTP 层 (Handler) ──依赖──→ Service 层 ──依赖──→ Repository 层 ──依赖──→ Model + │ │ │ + │ │ │ + ▼ ▼ ▼ + R (响应封装) R (响应封装) BaseRepository (基类) + 装饰器(BaseHandler) BaseRepository SQLAlchemy ORM + access_decorators Pydantic Schema +``` + +:::warning 依赖规则 +- HTTP 层只依赖 Service 层,不直接调用 Repository +- Service 层通过 Repository 访问数据库,不直接使用 `db.query()` +- Repository 层只依赖 Model 和数据库会话 +- 下层不感知上层存在,不调用上层方法 +::: + +## 完整调用链路示例 + +以添加岗位为例,展示各层之间的调用关系: + +``` +POST /api/v1/position/add +│ +├─ 1. Handler (handlers.py) +│ @permission_required("sys:position:add") +│ @check_demo +│ @operation_log("岗位管理", "添加") +│ async def post(self): +│ return await position_service.add(self) +│ +├─ 2. Service (service.py → base_service.py) +│ async def add(self, handler): +│ form = self._validate(handler, self.create_schema) # Pydantic 校验 +│ err = self._check_unique(form) # 唯一性校验 +│ self._before_add(handler, data) # 前置钩子 +│ fields = self._build_create_fields(data) # 字段组装 +│ self.repo.create(self.model(**fields)) # 调用 Repository +│ return R.ok(handler, msg="添加成功") +│ +├─ 3. Repository (base_repository.py) +│ def create(self, obj): +│ obj.create_user = str(self._uid()) # 自动写入创建人 +│ self.db.add(obj) +│ self.db.commit() +│ return obj +│ +├─ 4. Model (base_model.py) +│ class Position(BaseModel): +│ __tablename__ = "tornado_position" +│ name = Column(String(255), ...) +│ +└─ 5. Database + INSERT INTO tornado_position (name, status, sort, create_user, create_time) + VALUES ('高级工程师', 1, 10, '1', '2026-09-16 10:00:00') +``` + +## 各层文件职责一览 + +| 文件 | 层次 | 职责 | 是否包含业务逻辑 | +|:---|:---:|:---|:---:| +| `handlers.py` | HTTP | 路由、权限、日志、请求转发 | 否 | +| `schemas.py` | HTTP | 请求参数校验规则 | 否 | +| `service.py` | 业务 | 业务逻辑、校验、序列化 | 是 | +| `repository.py` | 数据 | 数据库 CRUD、查询构建 | 否 | +| `models.py` | 数据 | 表结构定义、字段映射 | 否 | + +## 总结 + +分层架构通过依赖单向和职责单一实现了良好的关注点分离。HTTP 层只关心请求接收和响应返回,Service 层只关心业务逻辑编排,Repository 层只关心数据访问。这种分层使得每一层都可以独立测试、独立替换,系统在模块数量增长时仍能保持清晰的代码结构。以岗位模块为例,完整实现一个 CRUD 模块只需 6 个文件(handlers + schemas + service + repository + models + api路由),其中 Repository 和 Model 几乎零代码,开发者只需关注 Service 层的业务差异点。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume3/overview.md b/DjangoAdmin_Tornado_EleVue/zh/volume3/overview.md new file mode 100644 index 0000000..7982902 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume3/overview.md @@ -0,0 +1,216 @@ +# 整体架构图 + +本章从全局视角阐述系统的整体技术架构,涵盖前端、后端、数据库及基础设施等核心组成部分。 + +## 架构总览 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Browser (Vue3 + ElementPlus) │ +│ Axios HTTP Request / Vite Dev Proxy │ +└──────────────────────────────┬──────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ Nginx (反向代理) │ +│ 静态资源 / SSL 终止 / 负载均衡 / gzip 压缩 │ +└──────────────────────────────┬──────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ Tornado Application │ +│ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ Handler 生命周期 │ │ +│ │ set_default_headers() → prepare() → handler_method() → on_finish()│ │ +│ │ CORS + trace_id JWT 认证 权限+日志+业务逻辑 释放DB会话 │ │ +│ └───────────────────────────────┬───────────────────────────────────┘ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ Service Layer (业务逻辑层) │ │ +│ │ BaseService / 自定义 Service │ │ +│ │ 唯一性校验 · 字段组装 · 序列化 · 文件处理 │ │ +│ └───────────────────────────────┬───────────────────────────────────┘ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ Repository Layer (数据访问层) │ │ +│ │ BaseRepository │ │ +│ │ CRUD · 软删除 · 分页 · 等值查询 · 原始条件查询 │ │ +│ └───────────────────────────────┬───────────────────────────────────┘ │ +│ ▼ │ +│ ┌─────────────────────┐ ┌──────────────────────────────────────────┐ │ +│ │ SQLAlchemy 2.0 │ │ Redis (同步客户端) │ │ +│ │ ORM → SQL 生成 │ │ Token 黑名单 · 登录锁 · 字典缓存 · 权限缓存│ │ +│ └──────────┬──────────┘ └──────────────────────────────────────────┘ │ +└─────────────┼───────────────────────────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ Database (MySQL / PostgreSQL / SQL Server / SQLite / Oracle)│ +│ 表前缀: tornado_ · 软删除: is_delete │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## 架构特征 + +### 1. 前后端分离 + +前端 Vue3 应用通过 HTTP API 调用后端 Tornado 服务,两者独立开发、独立部署。前端开发阶段通过 Vite 的 `proxy` 配置将 `/api` 请求代理到后端 `http://127.0.0.1:8041`,生产环境由 Nginx 统一反向代理。 + +### 2. 分层架构 + +后端严格分为四层,每层职责单一、依赖单向: + +| 层次 | 目录 | 职责 | +|:---:|:---:|:---| +| HTTP 层 | `modules/{module}/handlers.py` | 路由定义、权限装饰器、操作日志、响应返回 | +| 业务层 | `modules/{module}/service.py` | 业务逻辑、校验规则、序列化加工 | +| 数据层 | `modules/{module}/repository.py` | 数据库 CRUD、查询条件组装 | +| 模型层 | `modules/{module}/models.py` | 表结构定义、字段映射 | + +### 3. 模板方法模式 + +通过 `BaseRepository` 和 `BaseService` 基类,将通用 CRUD 流程固化为模板方法。子类只需声明差异点(过滤字段、排序规则、唯一性校验等),即可获得完整的增删改查能力。 + +### 4. Handler 生命周期 + +Tornado 通过 Handler 生命周期方法实现横切关注点: + +```python +# src/core/base_handler.py +class BaseHandler(tornado.web.RequestHandler): + def set_default_headers(self): + """① 设置 CORS 头 + trace_id""" + cros_required(self) + apply_trace_id(self) + + @login_required + def prepare(self): + """② JWT 认证(白名单 URL 跳过)""" + pass + + def on_finish(self): + """④ 请求结束后释放数据库会话""" + db.close() + + def write_error(self, status_code, **kwargs): + """⑤ 异常统一响应""" + ... +``` + +Handler 方法中通过装饰器实现权限/演示模式/操作日志: + +```python +class PositionAddHandler(BaseHandler): + @permission_required("sys:position:add") # RBAC 权限校验 + @check_demo # 演示模式拦截 + @operation_log("岗位管理", "添加") # 操作日志记录 + async def post(self): + return await position_service.add(self) +``` + +### 5. 全局异常处理 + +`BaseHandler.write_error()` 统一捕获异常并返回标准响应: + +| 异常类型 | 触发场景 | 响应 | +|:---|:---|:---| +| `AuthError` | JWT 过期 / 未登录 | `{"code": 401, "msg": "登录过期"}` | +| `PermissionDeniedError` | 权限不足 | `{"code": 403, "msg": "权限不足"}` | +| `BusinessError` | 业务逻辑错误 | `{"code": 1, "msg": "..."}` | +| `ValidationError` | Pydantic 校验失败 | `{"code": 1, "msg": "field: message"}` | +| `NotFoundError` | 记录不存在 | `{"code": 404, "msg": "记录不存在"}` | +| `Exception` | 未捕获异常 | `{"code": 500, "msg": "服务器内部错误"}` | + +### 6. 统一响应格式 + +所有 API 端点返回统一的 JSON 结构: + +```json +{ + "code": 0, + "data": {}, + "msg": "操作成功", + "ok": true +} +``` + +成功响应 `code=0`,失败响应 `code=1`(或其他业务码),前端无需针对不同接口做差异化解析。 + +### 7. 软删除策略 + +所有业务表通过 `is_delete` 字段实现软删除(0=正常,1=已删除)。`BaseRepository` 的等值查询方法自动追加 `is_delete=0` 过滤,业务代码无需关心已删除数据。 + +### 8. 事务管理 + +数据库事务由 Repository 层的 `create()` / `update()` / `batch_delete()` 方法内部 `db.commit()` 管理。Service 层的 `try/finally` 块确保 `repo.close()` 释放会话。`BaseHandler.on_finish()` 兜底关闭会话,避免长连接泄漏。 + +### 9. 多数据库驱动 + +通过 `DB_DRIVER` 环境变量切换数据库驱动(MySQL / PostgreSQL / SQL Server / SQLite / Oracle),SQLAlchemy 的方言层屏蔽了底层差异,业务代码无需修改。 + +### 10. 应用启动流程 + +```python +# app.py(入口)→ bootstrap.py(启动引导)→ src/app.py(应用工厂) +# 启动流程: +# 1. bootstrap.main() 解析 --port 参数 +# 2. create_app() 构建 Application(路由、模板、静态目录) +# 3. register_extensions() 初始化扩展(SQLAlchemy、Redis) +# 4. HTTPServer.listen(port) 开始监听 +# 5. init_scheduler() 启动定时任务调度器 +# 6. 注册 SIGINT/SIGTERM 信号实现优雅关闭 +# 7. ioloop.start() 启动事件循环 +``` + +## 技术分层图 + +``` +┌──────────────────────────────────────────────────────────────┐ +│ Frontend (Vue3) │ +│ ElementPlus · Pinia · Vue Router · Axios │ +└──────────────────────────┬───────────────────────────────────┘ + │ HTTP / JSON +┌──────────────────────────┴───────────────────────────────────┐ +│ Tornado Application │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────────┐ │ +│ │ Handler │ │ Router │ │ Exception Handler │ │ +│ │ ───────── │ │ ───────── │ │ ───────────────── │ │ +│ │ CORS │ │ auth │ │ BaseAppError │ │ +│ │ trace_id │ │ v1 (login │ │ write_error() │ │ +│ │ login_ │ │ required) │ │ 统一 {code,data,msg} │ │ +│ │ required │ │ │ │ │ │ +│ └─────────────┘ └─────────────┘ └──────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ Handlers (modules/{module}/handlers.py) │ │ +│ │ @permission_required · @check_demo · @operation_log │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ Services (modules/{module}/service.py) │ │ +│ │ BaseService · 自定义 Service │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ Repositories (modules/{module}/repository.py) │ │ +│ │ BaseRepository · 自定义 Repository │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ Models (modules/{module}/models.py) │ │ +│ │ BaseModel → SQLAlchemy ORM │ │ +│ └──────────────────────────────────────────────────────────┘ │ +└──────────────────────────┬───────────────────────────────────┘ + │ + ┌────────────┴────────────┐ + ▼ ▼ + ┌────────────┐ ┌─────────────┐ + │ Database │ │ Redis │ + │ (MySQL等) │ │ (同步客户端) │ + └────────────┘ └─────────────┘ +``` + +## 总结 + +系统采用经典的前后端分离 + 分层架构设计,通过 Handler 生命周期方法和装饰器实现横切关注点(认证、权限、日志),通过基类模板方法消除重复 CRUD 代码,通过 Pydantic 实现请求参数的声明式校验。整体架构在保持简洁的同时,具备良好的可扩展性和可维护性。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume3/request-flow.md b/DjangoAdmin_Tornado_EleVue/zh/volume3/request-flow.md new file mode 100644 index 0000000..cf85c54 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume3/request-flow.md @@ -0,0 +1,328 @@ +# 请求完整链路 + +本章以一个具体的 API 请求为例,详细描述从浏览器发起 HTTP 请求到返回响应的完整链路,帮助开发者理解每一层的职责和数据流转方式。 + +## 请求链路总览 + +:::tip 链路概览 +一个典型的 API 请求经过以下阶段:浏览器 → Nginx → Tornado → Handler 生命周期(set_default_headers → prepare → handler_method → on_finish)→ 响应返回浏览器 +::: + +以下以 `POST /api/v1/position/add` 添加岗位为例,逐步拆解完整链路。 + +## Tornado 请求处理模型 + +Tornado 通过 **Handler 生命周期方法** 和 **装饰器** 实现横切关注点: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Tornado Handler 生命周期 │ +│ │ +│ ① set_default_headers() → 设置 CORS 头 + trace_id │ +│ ② prepare() → @login_required(JWT 认证) │ +│ ③ handler method → @permission_required + @check_demo │ +│ + @operation_log + 业务逻辑 │ +│ ④ on_finish() → db.close()(释放数据库会话) │ +│ │ +│ 异常时:write_error() → 统一异常响应 + 日志记录 │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## 第一阶段:网络层 + +### 1. 浏览器发起请求 + +```javascript +// 前端 Axios 调用 +const res = await request({ + url: '/api/v1/position/add', + method: 'post', + data: { + name: '高级工程师', + status: 1, + sort: 10 + } +}); +``` + +前端通过 Axios 发起 HTTP POST 请求,携带: +- `Authorization: Bearer ` 请求头 +- `Content-Type: application/json` 请求头 +- JSON 请求体 + +### 2. Nginx 反向代理(生产环境) + +```nginx +location /api/ { + proxy_pass http://127.0.0.1:8041; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +} +``` + +Nginx 将请求代理到 Tornado 服务,同时附加客户端真实 IP 到 `X-Forwarded-For` 头。 + +## 第二阶段:Handler 生命周期 + +### 3. 路由匹配 + +Tornado 根据 HTTP 方法 + URL 路径匹配到对应的 Handler 类: + +```python +# src/api/v1/router.py +def build_routes() -> list: + routes = [] + for module in _RESOURCE_MODULES: + routes.extend([(V1_PREFIX + path, handler) for path, handler in module.routes]) + routes.append((V1_PREFIX + "/health", HealthHandler)) + return routes +``` + +路由表中 `POST /api/v1/position/add` 匹配到 `PositionAddHandler`。 + +### 4. set_default_headers() — 设置响应头 + +```python +# src/core/base_handler.py +class BaseHandler(tornado.web.RequestHandler): + def set_default_headers(self): + super().set_default_headers() + cros_required(self) # CORS 跨域头 + apply_trace_id(self) # 链路追踪 ID +``` + +- **CORS**:检查 `Origin` 头是否在白名单中,设置 `Access-Control-Allow-*` 响应头 +- **trace_id**:生成唯一追踪 ID,写入响应头和上下文变量 + +### 5. prepare() — JWT 认证 + +```python +# src/core/base_handler.py +class BaseHandler(tornado.web.RequestHandler): + @login_required + def prepare(self): + pass +``` + +`login_required` 装饰器在 `prepare()` 阶段执行认证: + +```python +# src/common/middleware/authentication.py +def login_required(func): + def wrapper(self, *args, **kwargs): + request_url = self.request.path + # 白名单(/login、/captcha)和 OPTIONS 预检直接放行 + if request_url not in IGNORE_URL and self.request.method != "OPTIONS": + # 1. 从 Authorization 头提取 Bearer Token + success, access_token, _ = get_access_token(self.request) + if not success or not access_token: + return R.failed(self, code=401, msg="登录过期") + # 2. JWT 解密验证(签名 + 有效期) + result = parse_payload(access_token) + if result["code"] != 0: + return R.failed(self, code=401, msg="登录过期") + # 3. 黑名单校验(Redis,不可用时 fail-open 放行) + if is_token_blacklisted(access_token): + return R.failed(self, code=401, msg="登录过期") + # 4. 将用户信息写入上下文变量 + data = result["data"] + current_user_id.set(int(data["userId"])) + current_username.set(data.get("username", "")) + current_realname.set(data.get("realname", "")) + return func(self, *args, **kwargs) + return wrapper +``` + +认证失败时直接返回 `{"code": 401, "msg": "登录过期"}`,不进入后续 Handler 方法。 + +### 6. Handler 方法 — 权限校验 + 业务处理 + +```python +# src/modules/position/handlers.py +class PositionAddHandler(BaseHandler): + @permission_required("sys:position:add") # ① RBAC 权限校验 + @check_demo # ② 演示模式拦截 + @operation_log("岗位管理", "添加") # ③ 操作日志记录 + async def post(self): + return await position_service.add(self) # ④ 调用 Service 层 +``` + +**装饰器执行顺序**(从外到内): + +``` +① permission_required → 校验用户是否拥有 sys:position:add 权限 + ├── user_id == 1(admin)→ 直接放行 + └── 其他用户 → 查询权限列表(Redis 缓存)→ 匹配则放行,否则返回 403 + +② check_demo → TORNADO_DEMO=True 时返回 "演示环境,暂无操作权限" + +③ operation_log → 记录操作日志(请求参数、操作人、IP、耗时等) + +④ handler method → 调用 position_service.add(self) +``` + +### 7. Service 层 — 业务逻辑 + +```python +# src/core/base_service.py +class BaseService: + async def add(self, handler) -> R: + try: + # 1. 解析请求体 JSON + Pydantic 校验 + form = self._validate(handler, self.create_schema) + data = form.model_dump() + # 2. 唯一性校验 + err = self._check_unique(form) + if err: + return R.failed(handler, err) + # 3. 前置扩展钩子 + self._before_add(handler, data) + # 4. 组装入库字段 + fields = self._build_create_fields(data) + # 5. 调用 Repository 创建记录 + self.repo.create(self.model(**fields)) + # 6. 返回成功响应 + return R.ok(handler, msg="添加成功") + finally: + self.repo.close() +``` + +### 8. Repository 层 — 数据访问 + +```python +# src/core/base_repository.py +class BaseRepository: + def create(self, obj: T) -> T: + # 自动写入创建人 + if hasattr(obj, "create_user"): + obj.create_user = str(self._uid()) + self.db.add(obj) + self.db.commit() + return obj +``` + +### 9. 响应返回 + +```python +# src/core/response.py +def ok(self, data=None, msg="操作成功", code=0, **kwargs): + body = {"code": code, "data": data, "msg": msg, "ok": True} + if kwargs: + body.update(kwargs) + self._api_result = body + self.finish(body) # Tornado 写出 JSON 响应 +``` + +`R.ok()` / `R.failed()` 内部调用 `self.finish(body)`,Tornado 自动序列化为 JSON 并发送 HTTP 响应。 + +### 10. on_finish() — 释放资源 + +```python +# src/core/base_handler.py +class BaseHandler(tornado.web.RequestHandler): + def on_finish(self): + try: + db.close() # 兜底关闭数据库会话 + except Exception: + pass +``` + +请求结束后关闭数据库会话,避免长连接泄漏。Service 层的 `finally` 块已关闭时此处为幂等 no-op。 + +### 11. 异常处理 + +```python +# src/core/base_handler.py +class BaseHandler(tornado.web.RequestHandler): + def write_error(self, status_code, **kwargs): + exc = kwargs.get("exc_info", (None, None, None))[1] + if isinstance(exc, BaseAppError): + # 业务异常:按类型分级记录日志 + self.set_status(exc.status_code) + self.finish({"code": exc.code, "data": None, "msg": exc.msg, "ok": False}) + else: + # 未预期异常:记录完整堆栈 + self.set_status(500) + self.finish({"code": 500, "data": None, "msg": "服务器内部错误", "ok": False}) +``` + +| 异常类型 | 触发场景 | 响应 | +|:---|:---|:---| +| `AuthError` | JWT 过期 / 无权限 | `{"code": 401, "msg": "登录过期"}` | +| `PermissionDeniedError` | 权限不足 | `{"code": 403, "msg": "权限不足"}` | +| `ValidationError` | Pydantic 校验失败 | `{"code": 1, "msg": "field: message"}` | +| `BusinessError` | 业务逻辑错误 | `{"code": 1, "msg": "..."}` | +| `NotFoundError` | 记录不存在 | `{"code": 404, "msg": "记录不存在"}` | +| `Exception` | 未捕获异常 | `{"code": 500, "msg": "服务器内部错误"}` | + +## 完整链路流程图 + +``` +[浏览器] + │ POST /api/v1/position/add + │ Authorization: Bearer + ▼ +[Nginx] ── 反向代理转发 + │ + ▼ +[Tornado 路由匹配] ── 匹配 PositionAddHandler + │ + ▼ +[set_default_headers()] ── 设置 CORS 头 + trace_id + │ + ▼ +[prepare() + @login_required] ── JWT 认证 + │ ├─ 白名单(/login、/captcha)→ 跳过 + │ ├─ Token 无效/过期 → 返回 {code:401, msg:"登录过期"} + │ ├─ Token 在黑名单中 → 返回 {code:401, msg:"登录过期"} + │ └─ 认证通过 → current_user_id / current_username / current_realname 写入上下文 + │ + ▼ +[@permission_required("sys:position:add")] ── RBAC 权限校验 + │ ├─ user_id == 1 → 直接放行 + │ ├─ 权限不足 → 返回 {code:403, msg:"权限不足"} + │ └─ 权限通过 → 继续 + │ + ▼ +[@check_demo] ── 演示模式拦截 + │ ├─ TORNADO_DEMO=True → 返回 {code:1, msg:"演示环境,暂无操作权限"} + │ └─ 非演示模式 → 继续 + │ + ▼ +[@operation_log("岗位管理", "添加")] ── 操作日志装饰器 + │ + ▼ +[async def post(self)] ── Handler 方法 + │ + ▼ +[position_service.add(self)] ── Service 层 + ├─ parse_json(handler) ── 解析请求体 JSON + ├─ validate(schema, data) ── Pydantic 校验 + ├─ _check_unique(form) ── 唯一性校验(DB 查询) + ├─ _before_add(handler, data) ── 前置扩展钩子 + ├─ _build_create_fields(data) ── 组装入库字段 + └─ repo.create(model(**fields)) ── Repository 层 + │ + ▼ + [BaseRepository.create(obj)] + ├─ obj.create_user = str(uid) ── 自动写入创建人 + ├─ db.add(obj) + db.commit() ── 提交事务 + └─ return obj + │ + ▼ +[R.ok(handler, msg="添加成功")] ── 统一响应 + ├─ self._api_result = body ── 保存结果供日志解析 + └─ self.finish(body) ── 写出 JSON 响应 + │ + ▼ +[on_finish()] ── 释放资源 + └─ db.close() ── 关闭数据库会话 + │ + ▼ +[浏览器] ← { "code": 0, "data": null, "msg": "添加成功", "ok": true } +``` + +## 总结 + +Tornado 的请求处理通过 Handler 生命周期方法(`set_default_headers` → `prepare` → `handler_method` → `on_finish`)和装饰器(`@permission_required`、`@check_demo`、`@operation_log`)实现横切关注点。认证在 `prepare()` 阶段完成,权限/演示模式/操作日志通过装饰器在 Handler 方法执行前校验,数据库会话在 `on_finish()` 中兜底关闭。这种设计使得每一层职责单一、可独立测试。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume3/security.md b/DjangoAdmin_Tornado_EleVue/zh/volume3/security.md new file mode 100644 index 0000000..67bb123 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume3/security.md @@ -0,0 +1,735 @@ +# 安全架构设计 + +本章详细阐述系统的安全架构设计,围绕认证、授权、防护、审计四大安全领域展开,旨在帮助开发者全面理解系统的安全保障体系,确保在开发与部署过程中遵循安全规范,有效防范各类安全风险。 + +**认证(Authentication)**:系统采用基于 JWT(JSON Web Token)的无状态认证机制,用户通过用户名密码登录后获取 Token,后续请求通过 `Authorization: Bearer ` 头传递,由 `login_required` 依赖统一拦截并验证 Token 的有效性(签名校验、有效期检查、Redis 黑名单过滤)。同时,系统内置登录失败锁定策略,连续失败 5 次即锁定账号 10 分钟,有效防御暴力破解攻击。 + +**授权(Authorization)**:基于 RBAC(基于角色的访问控制)模型设计,权限控制粒度细化至接口级。用户通过角色继承权限节点(如 `sys:position:add`),由 `@permission_required` 装饰器在路由层统一拦截校验,权限数据采用 Redis 缓存加速查询,管理员(`userId == 1`)自动放行。权限变更时,相关缓存主动失效,确保权限更新的实时性。 + +**防护(Protection)**:系统从多维度构建安全防护体系。限流中间件基于 Redis 滑动窗口算法,有效防御 CC 攻击和接口滥用;参数校验通过 Pydantic Schema 自动拦截非法输入,防止 SQL 注入和 XSS 攻击;敏感密码采用 bcrypt 加盐哈希存储,即使数据库泄露也无法还原明文;演示模式下 `@check_demo` 装饰器统一拦截写操作,保护测试数据不被污染;跨域配置精确控制可信域名,防止 CSRF 攻击。 + +**审计(Audit)**:系统提供完备的操作审计能力。操作日志中间件自动记录所有写操作(POST/PUT/DELETE/PATCH)的请求路径、参数、操作人、IP 地址及执行结果,存入 `tornado_operation_log` 表;登录日志独立记录每次登录尝试(含 IP、User-Agent、登录状态),便于安全事件追溯与异常行为分析;定时任务每次执行记录至 `tornado_job_log` 表,确保后台任务的执行过程可审计。 + +:::tip 安全设计原则 +纵深防御:认证、授权、限流、参数校验多层防护,单一防线失效时仍有其他机制兜底 + +最小权限:用户仅能访问已授权接口,角色权限按需分配,遵循"够用即可"原则 + +数据不落地:敏感信息(Token、密码)不在日志中明文记录,密码哈希不可逆 + +安全默认:框架默认配置为安全模式(如 DEBUG 关闭、限流开启),如需放宽需显式配置 +::: + +## 安全架构总览 + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ 安全防御层次 │ +│ │ +│ ┌───────────────────────────────────────────────────────────────┐ │ +│ │ 网络层防护 │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ +│ │ │ CORS │ │ 限流 │ │ XSS 防护 │ │ 上传体积限制 │ │ │ +│ │ │ 跨域控制 │ │ 滑动窗口 │ │ 输入清洗 │ │ DoS 防御 │ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌───────────────────────────────────────────────────────────────┐ │ +│ │ 认证层 │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ +│ │ │ JWT 认证 │ │ 验证码 │ │ 登录锁定 │ │ Token 黑名单 │ │ │ +│ │ │ Bearer │ │ 图形验证 │ │ 失败计数 │ │ 登出失效 │ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌───────────────────────────────────────────────────────────────┐ │ +│ │ 授权层 │ │ +│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ │ +│ │ │ RBAC 权限 │ │ 演示模式保护 │ │ 超级管理员跳过 │ │ │ +│ │ │ permission_ │ │ check_demo │ │ user_id=1 bypass │ │ │ +│ │ │ required │ │ │ │ │ │ │ +│ │ └──────────────┘ └──────────────┘ └──────────────────────┘ │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌───────────────────────────────────────────────────────────────┐ │ +│ │ 数据层 │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ +│ │ │ 密码加密 │ │ 参数校验 │ │ SQL 注入 │ │ 操作日志 │ │ │ +│ │ │ bcrypt │ │ Pydantic │ │ ORM 防护 │ │ 审计追踪 │ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## 一、认证机制 + +### JWT 令牌认证 + +:::tip JWT 工作流程 +用户登录成功后,服务端签发 JWT Token 返回给前端。前端将 Token 存储在本地,后续每次请求通过 `Authorization: Bearer ` 头携带。服务端验证 Token 签名和有效期,解析出用户信息。 +::: + +1. 工作流程时序图 + +![](/volume3/security/jwt_token.png) + + +2. 纯文本树状时序图 + +``` +[前端] [后端] [Redis] + │ │ │ + │── POST /login ─────────→│ │ + │ {username, pwd, │ │ + │ captcha, uuid} │ │ + │ │── 验证码校验 (uuid) ──→│ + │ │←─ captcha_text ────────│ + │ │ │ + │ │── 密码校验 │ + │ │── 签发JWT │ + │ │── 存储Token ──────────→│ + │←─ {token} ─────────────│ │ + │ │ │ + │── GET /api/v1/xxx ────→│ │ + │ Authorization: │ │ + │ Bearer │ │ + │ │── 验证JWT │ + │ │── 检查黑名单 ─────────→│ + │ │←─ exists ─────────────│ + │←─ {data} ──────────────│ │ +``` + +:::tip 流程说明 +1. 前端提交用户名、密码、验证码及 UUID +2. 后端校验验证码,验证密码,生成 JWT 并缓存至 Redis +3. 前端后续请求携带 JWT,后端校验 Token 有效性并检查黑名单 +4. 校验通过后返回业务数据 +::: + +#### JWT 配置 + +```python +# src/config/auth.py +# ============================================================ +# JWT 配置 +# ============================================================ +# JWT签名密钥(必须通过环境变量设置,不能使用默认值上线) +JWT_SECRET = os.getenv('JWT_SECRET', '') +# JWT令牌过期时间(分钟) +JWT_EXPIRE_MINUTES = int(os.getenv('JWT_EXPIRE_MINUTES', '20')) +JWT_ALGORITHM = "HS256" # 签名算法 +``` + +#### Token 签发 + +```python +# src/core/jwt.py +def create_token(payload, timeout=None): + """ + 生成JWT令牌 + ============================================================= + + 根据传入的载荷数据生成JWT访问令牌,自动添加过期时间。 + 使用强密钥确保令牌安全性,符合RFC 7518安全规范。 + + 参数: + payload (dict): JWT载荷数据,通常包含用户ID、用户名等信息 + timeout (int): 令牌过期时间(分钟),默认使用DEFAULT_TIMEOUT_MINUTES + + 返回: + str: 生成的JWT令牌字符串 + + 使用示例: + >>> payload = {'userId': 1001, 'username': 'admin'} + >>> token = create_token(payload, timeout=30) + >>> print(token) + eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... + + 注意事项: + 1. 密钥长度必须至少32字节,否则会发出警告 + 2. 生产环境建议通过环境变量设置JWT_SECRET + 3. 令牌过期后需要重新登录获取新令牌 + """ + try: + # 设置过期时间 + # 说明:在载荷中添加exp字段,指定令牌的过期时间点 + # 计算方式:当前UTC时间 + 指定的分钟数 + if timeout is None: + timeout = DEFAULT_TIMEOUT_MINUTES + + # 创建payload副本,避免修改原始数据 + payload_copy = payload.copy() + + # 添加过期时间(exp)和签发时间(iat) + # exp: 过期时间,超过此时间令牌无效 + # iat: 签发时间,用于令牌生命周期计算 + current_time = datetime.datetime.now(tz=datetime.timezone.utc) + payload_copy['exp'] = current_time + datetime.timedelta(minutes=timeout) + payload_copy['iat'] = current_time + + # 声明类型和加密算法 + # 说明:定义JWT头部信息,包含令牌类型和加密算法 + headers = { + "typ": "JWT", # 令牌类型 + "alg": JWT_ALGORITHM # 加密算法 + } + + # 生成JWT令牌 + # 函数:jwt.encode() + # 参数说明: + # payload: JWT载荷数据 + # key: 加密密钥(盐) + # algorithm: 加密算法(HS256) + # headers: JWT头部信息 + token = jwt.encode( + payload=payload_copy, + key=JWT_SECRET, + algorithm=JWT_ALGORITHM, + headers=headers + ) + + # 记录日志(可选,生产环境建议使用DEBUG级别) + logger.debug(f"生成JWT令牌成功,过期时间:{timeout}分钟") + + return token + + except Exception as e: + # 捕获并记录生成令牌时的异常 + logger.error(f"生成JWT令牌失败: {str(e)}") + raise +``` + +#### Token 验证 + +```python +# src/common/middleware/authentication.py +# ============================================================ +# 登录认证装饰器 +# ============================================================ +def login_required(func): + """登录检测装饰器:校验 JWT(含黑名单)并将 userId 写入上下文。""" + def wrapper(self, *args, **kwargs): + request_url = self.request.path + if request_url not in IGNORE_URL and self.request.method != "OPTIONS": + success, access_token, _ = get_access_token(self.request) + if not success or not access_token: + return R.failed(self, code=401, msg="登录过期") + result = parse_payload(access_token) + if result["code"] != 0: + return R.failed(self, code=401, msg="登录过期") + if is_token_blacklisted(access_token): + return R.failed(self, code=401, msg="登录过期") + data = result["data"] + current_user_id.set(int(data["userId"])) + current_username.set(data.get("username", "")) + current_realname.set(data.get("realname", "")) + return func(self, *args, **kwargs) + return wrapper + raise AuthorizationException(code=401, msg=result['msg']) + + data = result['data'] + user_id = int(data.get('userId', 0)) + + # 用户有效状态校验:优先读 Redis 缓存(状态变更/删除时已即时失效), + # 未命中再回源 DB 并回填缓存,避免每请求查库;Redis 不可用时缓存读失败自动回源 DB + from utils.perm_cache import get_cached_user_active, set_cached_user_active + active = await get_cached_user_active(user_id) + if active is None: + from modules.user.repository import UserRepository + user = UserRepository().get_by_id, user_id) + active = bool(user and user.status == 1) + await set_cached_user_active(user_id, active) + + if not active: + raise AuthorizationException(code=401, msg="用户已被禁用或删除,请联系管理员") + + # 写入上下文变量(contextvars),供仓储/服务层读取 + current_user_id.set(user_id) + current_username.set(data.get('username', '')) + current_realname.set(data.get('realname', '')) + + # 滑动窗口续签:token 生命过半时自动签发新 token,通过响应头返回 + await _maybe_refresh_token(request, access_token, data) +``` + +#### Token 黑名单 + +用户登出时,将当前 Token 加入 Redis 黑名单,TTL 等于 Token 剩余有效期: + +```python +# src/modules/auth/handlers.py +# ============================================================ +# 退出登录 +# ============================================================ +class LogoutHandler(BaseHandler): + async def get(self): + """用户退出登录:将当前访问令牌加入黑名单。""" + success, access_token, _ = get_access_token(self.request) + if success and access_token: + add_token_to_blacklist(access_token) + + # 记录退出登录日志 + username = get_username(request) + realname = get_realname(request) + login_log.record_login_log, request, username, 2, 0, 0, method='v1.logout', + request_method=request.method, create_user=realname, result="退出成功") + return R.ok(msg="注销成功") +``` + +### 登录验证码 + +:::tip 验证码防暴力破解 +每次登录前需先获取验证码,验证码存储在 Redis 中(TTL 120 秒),登录时校验验证码正确性。验证码使用 UUID 作为 key,避免被猜测。 +::: + +```python +# 获取验证码 +GET /api/v1/captcha +Response: { "key": "xxx", "captcha": "data:image/png;base64,iVBORw0KG..." } + +# 登录时校验 +POST /api/v1/login +Body: { "username": "admin", "password": "123456", "code": "a3Xb", "key": "xxx" } +``` + +### 登录失败锁定 + +连续 5 次登录失败后,账号自动锁定 10 分钟: + +```python +# src/modules/auth/service.py +async def login(request, data): + redis = get_redis() + + # 检查锁定状态 + lock_key = f"login:lock:{data.username}" + is_locked = redis.get(lock_key) + if is_locked: + return R.failed("账号已锁定,请10分钟后再试") + + # 验证用户名密码 + user = UserRepository().get_one, username=data.username) + if not user or not verify_password(data.password, user.password): + # 原子递增失败计数 + fail_key = f"login:fail:{data.username}" + count = await incr_with_expire(redis, fail_key, 300) + if count >= 5: + redis.set(lock_key, 1, ex=600) # 锁定 10 分钟 + return R.failed(f"用户名或密码错误,剩余{5-count}次机会") + + # 登录成功,清除失败计数 + redis.delete(f"login:fail:{data.username}") +``` + +### 密码安全 + +```python +# src/core/password.py +# +====================================================================== +# | 模块: 密码加密工具 +# | 说明: 基于 bcrypt 的双重加盐方案 +# +====================================================================== + +"""密码加密工具:基于 bcrypt 的双重加盐方案。""" + +import secrets + +import bcrypt + +# bcrypt 工作因子,值越大计算越慢越安全,默认12(约250ms) +BCRYPT_ROUNDS = 12 + + +# ============================================================ +# 密码工具函数 +# ============================================================ +def generate_salt(length: int = 10) -> str: + """生成随机盐值""" + return secrets.token_urlsafe(length)[:length] + + +def encrypt_password(password: str, salt: str) -> str: + """使用 bcrypt 加密密码(外部 salt 双重加盐)""" + combined = (password + salt).encode('utf-8') + hashed = bcrypt.hashpw(combined, bcrypt.gensalt(rounds=BCRYPT_ROUNDS)) + return hashed.decode('utf-8') + + +def verify_password(password: str, salt: str, hashed: str) -> bool: + """验证密码是否正确""" + combined = (password + salt).encode('utf-8') + return bcrypt.checkpw(combined, hashed.encode('utf-8')) +``` + +- 使用 bcrypt 算法,每次哈希自动生成随机盐 +- 密码不可逆,数据库泄露后无法还原明文 +- 默认密码在首次登录时强制修改 + +## 二、授权机制 + +### RBAC 权限模型 + +``` +用户(User) ──N:M──→ 角色(Role) ──N:M──→ 菜单/权限(Menu) + │ │ │ + │ user_role │ role_menu │ permission + │ 关联表 │ 关联表 │ 权限标识 + └────────────────────┘ │ + ▼ + sys:user:add + sys:user:page + sys:role:update + ... +``` + +权限标识格式:`sys:{module}:{action}` + +- `sys`:系统前缀 +- `{module}`:模块名(user / role / menu / position / level 等) +- `{action}`:操作名(page / list / detail / add / update / delete / status / batchDelete / export / import) + +### permission_required 装饰器 + +```python +# src/common/middleware/access_decorators.py +# ============================================================ +# 辅助函数 +# ============================================================ +def _extract_request(*args, **kwargs): + """从函数参数中提取 Request 对象""" + req = kwargs.get('request') + if req is not None: + return req + for arg in args: + if isinstance(arg, Request): + return arg + return None + + +# ============================================================ +# 节点权限鉴权装饰器 +# ============================================================ +def permission_required(permission: str): + """节点权限鉴权装饰器,userId==1(管理员)直接放行""" + + def decorator(func): + def _check(self): + """同步权限校验(直查DB,无Redis依赖,供线程池/回退场景使用)""" + userId = current_user_id.get() + if userId == 1: + return None + # 懒加载业务 service,避免 core → 业务模块顶层依赖 + from menu import service as menu + permission_list = menu.get_permissions_list_sync(userId) + if permission not in permission_list: + return R.failed("权限不足") + return None + + async def _check_cached(self): + """异步权限校验(走 Redis 缓存,未命中才回源DB)""" + userId = current_user_id.get() + if userId == 1: + return None + # 懒加载业务 service,避免 core → 业务模块顶层依赖 + from menu import service as menu + permission_list = await menu.get_permissions_list(userId) + if permission not in permission_list: + return R.failed("权限不足") + return None + + if asyncio.iscoroutinefunction(func): + @wraps(func) + async def async_wrapper(*args, **kwargs): + request = _extract_request(*args, **kwargs) + denied = await _check_cached(request) + if denied: + return denied + return await func(*args, **kwargs) + + return async_wrapper + + @wraps(func) + def sync_wrapper(*args, **kwargs): + request = _extract_request(*args, **kwargs) + # 同步端点由 Tornado 在线程池中执行,直接走同步直查 DB 校验; + # 不再每请求 asyncio.run 新建事件循环(避免跨 loop 复用 Redis 连接 + # 被静默吞掉、缓存形同虚设的性能与正确性问题) + denied = _check(request) + if denied: + return denied + return func(*args, **kwargs) + + return sync_wrapper + + return decorator +``` + +:::tip 同步/异步双通道 +装饰器根据被装饰函数的类型自动选择校验通道: +- `async def` 端点 → `_check_cached` 走 Redis 缓存(异步) +- `def` 端点 → `_check` 直查 DB(同步,在 Tornado 线程池中执行) + +这样避免了在同步端点中 `asyncio.run()` 新建事件循环导致的跨 loop 复用 Redis 连接问题。 +::: + +### 权限缓存 + +权限缓存采用「用户级缓存 + 全局版本号」双层策略(`src/common/permission.py`): + +```python +# src/common/permission.py +_VERSION_KEY = 'perm:version' # 全局版本号 +_PERM_PREFIX = 'perm:list:' # 用户权限列表前缀 +_MENU_PREFIX = 'perm:menu:' # 用户菜单树前缀 +_CACHE_TTL = 7200 # 缓存过期时间(2小时) + +async def get_cached_permissions(user_id: int): + """获取缓存的用户权限列表,未命中返回 None""" + redis = get_redis() + ver = await _get_version() + key = f'{_PERM_PREFIX}{user_id}:{ver}' + data = redis.get(key) + if data is not None: + return json.loads(data) + return None + +async def set_cached_permissions(user_id: int, perm_list: list): + """缓存用户权限列表(空列表不缓存,避免缓存空壳后无法感知后续数据变更)""" + if not perm_list: + return + redis = get_redis() + ver = await _get_version() + key = f'{_PERM_PREFIX}{user_id}:{ver}' + redis.set(key, json.dumps(perm_list), ex=_CACHE_TTL) +``` + +缓存失效策略: + +```python +# src/common/permission.py +async def invalidate_menu_change(): + """菜单增删改时调用,递增全局版本号使所有用户缓存自然失效""" + await _incr_version() + +async def invalidate_user_perms(user_id: int): + """用户角色变更时调用,删除该用户的权限和菜单缓存""" + redis = get_redis() + ver = await _get_version() + keys = [ + f'{_PERM_PREFIX}{user_id}:{ver}', + f'{_MENU_PREFIX}{user_id}:{ver}', + ] + redis.delete(*keys) + +async def invalidate_role_perms(role_id: int): + """角色菜单变更时调用,清理该角色下所有用户的权限和菜单缓存""" + # 查询该角色下所有用户 ID + user_ids = _query_user_ids, role_id) + if not user_ids: + return + redis = get_redis() + ver = await _get_version() + keys = [] + for uid in user_ids: + keys.append(f'{_PERM_PREFIX}{uid}:{ver}') + keys.append(f'{_MENU_PREFIX}{uid}:{ver}') + if keys: + redis.delete(*keys) +``` + +:::tip 版本号策略的优势 +传统方案在菜单变更时需要遍历所有用户删除缓存,版本号方案只需递增一个全局 key,所有用户的旧版本缓存自然失效(key 中包含版本号,读取时用新版本号拼 key,旧 key 不再被访问,等 TTL 过期自动清除)。 +::: + +### check_demo 装饰器 + +```python +# src/common/middleware/access_decorators.py +def check_demo(func): + """演示模式拦截装饰器,放在 @permission_required 之后、路由函数之前""" + if asyncio.iscoroutinefunction(func): + @wraps(func) + async def async_wrapper(*args, **kwargs): + if TORNADO_DEMO: + return R.failed("演示环境,暂无操作权限") + return await func(*args, **kwargs) + + return async_wrapper + + @wraps(func) + def sync_wrapper(*args, **kwargs): + if TORNADO_DEMO: + return R.failed("演示环境,暂无操作权限") + return func(*args, **kwargs) + + return sync_wrapper +``` + +- 保护所有写操作(add / update / delete / status / batchDelete / import) +- 在 `permission_required` 之后应用 +- 根据被装饰函数类型自动选择同步/异步包装 +- `TORNADO_DEMO=True` 时,所有写操作返回失败 + +## 三、网络层防护 + +### CORS 跨域控制 + +```python +# src/common/middleware/cors.py +from config.app import TORNADO_CORS_ORIGINS + +_ALLOWED_ORIGINS = {o.strip() for o in TORNADO_CORS_ORIGINS.split(",") if o.strip()} + +def cros_required(self): + """为响应设置跨域相关头。""" + origin = self.request.headers.get("Origin", "") + if _ALLOWED_ORIGINS: + if origin in _ALLOWED_ORIGINS: + self.set_header("Access-Control-Allow-Origin", origin) + self.set_header("Access-Control-Allow-Credentials", "true") + self.set_header("Vary", "Origin") + else: + self.set_header("Access-Control-Allow-Origin", "*") + self.set_header("Access-Control-Allow-Headers", "*") + self.set_header("Access-Control-Expose-Headers", "*") + self.set_header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") + self.set_header("Access-Control-Max-Age", "3600") +``` + +在 `BaseHandler.set_default_headers()` 中调用,每个请求自动设置 CORS 头。 + +- 生产环境应配置具体的 `TORNADO_CORS_ORIGINS`,避免使用 `*` +- 配置白名单后仅对匹配来源反射来源并允许携带凭据 + +### XSS 防护 + +```python +# src/common/utils/rich_text.py +import bleach + +def sanitize_html(content: str) -> str: + """XSS 清洗:移除危险标签和属性""" + allowed_tags = [ + 'p', 'br', 'strong', 'em', 'u', 'ol', 'ul', 'li', + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', + 'a', 'img', 'table', 'tr', 'td', 'th', 'tbody', 'thead' + ] + allowed_attrs = { + 'a': ['href', 'title'], + 'img': ['src', 'alt', 'width', 'height'], + } + return bleach.clean(content, tags=allowed_tags, attributes=allowed_attrs) +``` + +- 富文本内容入库前进行 XSS 清洗 +- 使用 `bleach` 库白名单过滤危险标签和属性 +- 图片 `src` 属性只允许合法 URL + +- 按 `Content-Length` 提前拒绝超大请求体 +- 避免超大 multipart 落临时盘后再由业务层校验 + +## 四、数据层防护 + +### SQL 注入防护 + +:::tip ORM 天然防注入 +SQLAlchemy ORM 使用参数化查询(Parameterized Query),所有用户输入通过参数绑定传递,不直接拼接 SQL 字符串,从根本上防止 SQL 注入。 +::: + +```python +# SQLAlchemy 参数化查询示例 +UserRepository().get_one(username="admin") +# 实际执行:SELECT * FROM tornado_user WHERE username = :username AND is_delete = 0 +# 参数绑定:{'username': 'admin'} +``` + +### 参数校验 + +```python +# Pydantic 声明式校验 +class PositionForm(BaseSchema): + name: str = Field(..., min_length=1, max_length=150) + status: int = Field(..., ge=1, le=2) + sort: int = Field(..., ge=0, le=99999) + + @field_validator('name') + @classmethod + def validate_name(cls, v): + if not v.strip(): + raise ValueError('岗位名称不能为空') + return v.strip() +``` + +- 所有请求参数通过 Pydantic 声明式校验 +- 自动拒绝不符合约束的请求 +- 校验错误统一返回格式 + +### 敏感字段保护 + +- 密码使用 bcrypt 哈希存储,不可逆 +- `to_dict()` 序列化时自动排除 `is_delete`、`password`、`salt` +- JWT 密钥通过环境变量配置,不硬编码 + +## 五、审计追踪 + +### 操作日志 + +```python +# src/common/middleware/operation_log.py +def operation_log(module: str, action: str): + """操作日志记录装饰器:自动记录写操作的模块、操作类型、执行耗时。""" + def decorator(func): + @wraps(func) + async def wrapper(self, *args, **kwargs): + start = time.time() + result = await func(self, *args, **kwargs) + duration = time.time() - start + # 记录操作日志 + log_data = { + 'module': module, + 'action': action, + 'ip': get_client_ip(self.request), + 'duration': int(duration * 1000), + } + save_operation_log(log_data) + return response +``` + +自动记录所有写操作的日志,包括: +- 操作人、操作模块、操作动作 +- 请求 URL、请求参数 +- 操作 IP、操作时间 +- 响应状态、执行耗时 + +### 登录日志 + +每次登录/登出自动记录到 `tornado_login_log` 表,包括登录账号、IP、浏览器、操作系统、登录状态等。 + +## 安全配置清单 + +| 配置项 | 环境变量 | 默认值 | 说明 | +|:---|:---|:---|:---| +| JWT 密钥 | `JWT_SECRET` | 必须配置 | HS256 签名密钥 | +| JWT 过期时间 | `JWT_EXPIRE_MINUTES` | 20 | Token 有效期(分钟) | +| 密码错误锁定 | — | 5 次/10 分钟 | 连续失败后锁定 | +| 验证码有效期 | — | 120 秒 | 验证码 Redis TTL | +| 限流开关 | `RATE_LIMIT_ENABLED` | False | 滑动窗口限流 | +| 限流阈值 | `RATE_LIMIT_LIMIT` | 100 | 每窗口最大请求数 | +| 限流窗口 | `RATE_LIMIT_WINDOW_SECONDS` | 60 | 窗口时长(秒) | +| CORS 源 | `CORS_ORIGINS` | * | 允许的跨域源 | +| 上传限制 | `MAX_UPLOAD_SIZE` | 10MB | 最大上传体积 | +| 演示模式 | `TORNADO_DEMO` | False | 锁定写操作 | +| 数据库密码 | `DB_PASSWORD` | — | 数据库连接密码 | +| Redis 密码 | `REDIS_PASSWORD` | — | Redis 连接密码 | + +:::warning 生产环境安全检查 +1. `JWT_SECRET` 必须使用强随机字符串,不能使用默认值 +2. `CORS_ORIGINS` 必须配置具体的域名,不能使用 `*` +3. `TORNADO_DEMO` 在生产环境应为 `False` +4. 数据库和 Redis 必须设置密码 +5. 启用 HTTPS(由 Nginx/负载均衡器处理) +::: + +## 总结 + +的安全架构涵盖认证(JWT + 验证码 + 登录锁定 + Token 黑名单)、授权(RBAC + permission_required + check_demo)、防护(CORS + 限流 + XSS + 上传限制 + SQL 注入防护)、审计(操作日志 + 登录日志)四大领域。通过纵深防御策略,在网络层、认证层、授权层、数据层分别设置安全屏障,确保系统在面对各类安全威胁时具备足够的防护能力。生产部署时需特别关注 JWT 密钥强度、CORS 配置、HTTPS 启用等关键安全项。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume3/tech-stack.md b/DjangoAdmin_Tornado_EleVue/zh/volume3/tech-stack.md new file mode 100644 index 0000000..4365e9f --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume3/tech-stack.md @@ -0,0 +1,133 @@ +# 技术选型说明 + +本章详细阐述系统各技术组件的选型理由与版本约束说明,旨在帮助开发者理解技术选型的依据与权衡。 + +## 后端框架选型 + +### Tornado + +:::tip 为什么选择 Tornado? +Tornado 是 Python 生态中成熟的异步 Web 框架,基于自有事件循环实现非阻塞 I/O。对于后台管理系统这类以 CRUD 为主的场景,Tornado 的轻量灵活和稳定可靠使其成为理想选择。 +::: + +| 特性 | 说明 | +|:---|:---| +| **原生异步** | 基于自有事件循环,`async/await` 一等公民,充分利用 I/O 等待时间 | +| **请求校验** | Pydantic v2 声明式校验,Service 层自动执行 | +| **类型安全** | 全链路类型提示,IDE 补全完善 | +| **轻量灵活** | 核心概念少,无强依赖,学习曲线平缓 | +| **稳定可靠** | 成熟的生产级框架,广泛应用于高并发场景 | + +### 选择 Tornado 的核心理由 + +1. **原生异步**:后台管理系统需要处理并发请求(文件上传、批量操作、定时任务),Tornado 的原生异步模型能充分利用 I/O 等待时间 +2. **Pydantic 校验**:声明式的数据校验,减少参数校验代码量 +3. **SQLAlchemy 解耦**:ORM 不与框架强绑定,支持多数据库驱动切换(MySQL / PostgreSQL / SQL Server / SQLite / Oracle) +4. **轻量灵活**:无强依赖,核心概念少,易于理解和扩展 +5. **稳定可靠**:经过多年生产验证的成熟框架 + +## ORM 选型 + +### SQLAlchemy 2.0 + +:::tip 为什么选择 SQLAlchemy? +SQLAlchemy 是 Python 生态中最成熟、功能最完备的 ORM 框架,2.0 版本统一了 Core 和 ORM 的 API 风格,同时保持了对多种数据库方言的原生支持。 +::: + +| 特性 | 说明 | +|:---|:---| +| **声明式模型** | 基于 `declarative_base()` 定义模型类,字段与数据库列一一映射 | +| **多数据库方言** | 通过 `DB_DRIVER` 切换 MySQL / PostgreSQL / SQL Server / SQLite / Oracle | +| **会话管理** | `scoped_session` + thread-local 实现请求级会话隔离 | +| **连接池** | 内置连接池(`QueuePool`),支持 `POOL_SIZE` / `MAX_OVERFLOW` 配置 | +| **软删除** | 通过 `is_delete` 字段 + Repository 基类自动过滤实现 | +| **读写分离** | 支持写引擎 + 读引擎分离,通过 `READ_DATABASE_URL` 配置 | + +版本约束:`SQLAlchemy >= 2.0.51` + +## 请求校验选型 + +### Pydantic v2 + +| 特性 | 说明 | +|:---|:---| +| **声明式校验** | 通过 `BaseModel` + `Field(...)` 声明字段约束,自动校验 | +| **自定义验证器** | `@field_validator` 支持复杂的业务校验逻辑 | +| **高性能** | v2 核心用 Rust 重写,校验速度较 v1 提升 5-50 倍 | +| **错误聚合** | Service 层 `validate()` 方法将校验错误提取为统一提示 | + +版本约束:`pydantic >= 2.13.4` + +## 前端技术栈 + +### Vue3 + Vite + ElementPlus + +| 组件 | 版本 | 选型理由 | +|:---|:---|:---| +| **Vue3** | 3.x | Composition API、更好的 TypeScript 支持 | +| **Vite** | 5.x | 极速 HMR、ESM 原生支持、开箱即用的 TypeScript 支持 | +| **ElementPlus** | 2.x | Vue3 生态最成熟的 UI 组件库,企业级后台管理系统首选 | +| **Pinia** | 2.x | Vue3 官方推荐的状态管理方案,替代 Vuex | +| **Vue Router** | 4.x | Vue3 官方路由,支持动态路由、路由守卫 | +| **Axios** | 1.x | HTTP 客户端,支持请求/响应拦截器、取消请求 | + +## 缓存与消息中间件 + +### Redis + +:::tip Redis 在系统中的角色 +Redis 在系统中承担多个关键职责:JWT 令牌黑名单、登录失败锁定、数据字典缓存、权限缓存等。采用同步客户端(`redis`)与全站同步 DB/Redis 调用风格一致。 +::: + +| 用途 | 数据结构 | 过期策略 | +|:---|:---|:---| +| JWT 黑名单 | String | 等于 Token 剩余有效期 | +| 登录失败锁 | String | 指定锁定时长 | +| 数据字典缓存 | String (JSON) | TTL 可配置 | +| 权限列表缓存 | String (JSON) | 变更时主动失效(按版本号) | + +版本约束:`redis >= 5.0.0`(同步客户端) + +## 定时任务 + +### APScheduler + +| 特性 | 说明 | +|:---|:---| +| **调度策略** | 支持 Cron、Interval、Date 三种触发器 | +| **持久化** | 任务定义存储在数据库,重启后自动恢复 | +| **日志记录** | 每次执行记录到 `tornado_job_log` 表 | + +## 密码安全 + +### bcrypt 双盐方案 + +| 特性 | 说明 | +|:---|:---| +| **双盐机制** | 系统生成随机盐 + bcrypt 自带盐,双重保护 | +| **自动升级** | 登录时检测旧版无盐哈希,自动升级为双盐方案 | +| **安全存储** | 盐值存储在用户表 `salt` 字段,哈希存储在 `password` 字段 | + +## 其他关键依赖 + +| 依赖 | 用途 | +|:---|:---| +| `PyJWT` | JWT 令牌签发与验证(HS256 算法),`src/core/jwt.py` | +| `bcrypt` | 密码哈希(带随机盐),`src/core/password.py` | +| `openpyxl` | Excel 导入导出 | +| `Pillow` | 验证码图片生成,`src/common/utils/captcha.py` | +| `python-dotenv` | `.env` 配置文件加载 | +| `alembic` | 数据库迁移,`migrations/` 目录 | +| `APScheduler` | 定时任务调度,`src/modules/job/scheduler.py` | +| `bleach` | 富文本 XSS 清洗,`src/common/utils/rich_text.py` | +| `ip2region` | IP 地理位置解析,`src/common/utils/ip2region.py` | + +## 版本锁定策略 + +:::warning 版本管理 +所有依赖版本在 `pyproject.toml` 中锁定(Poetry 管理),`requirements.txt` 为 UTF-8 降级副本。生产环境建议使用 `poetry export` 生成精确版本锁文件。 +::: + +## 总结 + +项目的技术选型以「高性能、类型安全、可扩展」为核心原则。Tornado + SQLAlchemy 2.0 + Pydantic v2 构成后端铁三角,Vue3 + Vite + ElementPlus 提供现代化前端体验,Redis 承担缓存与中间件职责。各组件版本经过充分测试验证,形成稳定的技术底座。选型时优先考虑组件的独立性和可替换性,避免强耦合,为未来的架构演进预留空间。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume3/tornado-async.md b/DjangoAdmin_Tornado_EleVue/zh/volume3/tornado-async.md new file mode 100644 index 0000000..4e0f11f --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume3/tornado-async.md @@ -0,0 +1,194 @@ +# Tornado IOLoop 与异步模型 + +本章详细说明 Tornado 的异步编程模型,包括 IOLoop 事件循环、协程使用方式、与同步 SQLAlchemy 的协作,以及需要避免的反模式。 + +:::tip 核心原则 +Tornado 基于单线程事件循环(IOLoop),Handler 方法默认为协程(`async def`)。正确理解 IOLoop 与同步阻塞操作的关系,是避免事件循环阻塞、保证系统并发性能的关键。 +::: + +## Tornado 的执行模型 + +Tornado 使用单线程 IOLoop 事件循环处理请求: + +``` +IOLoop 事件循环 + │ + ├─ 请求1 进入 → Handler.get() 协程 → await → 挂起 + │ │ + ├─ 请求2 进入 → Handler.get() 协程 → await → 挂起 + │ │ + ├─ 请求1 的 await 完成 → 恢复执行 → 返回响应 + │ + ├─ 请求2 的 await 完成 → 恢复执行 → 返回响应 + │ + └─ ... +``` + +Tornado **不会**自动将同步函数放入线程池。在 `async def` 中直接调用同步阻塞操作会阻塞整个 IOLoop。本项目的 DB 和 Redis 操作均为同步调用,通常毫秒级完成,短暂阻塞在实际业务中可接受。 + +## 决策树 + +``` +Handler/Service 需要做什么? +│ +├─ 包含 await 操作(Redis / 异步 HTTP / 异步文件读取) +│ └─ 使用 async def + await +│ +├─ 纯同步操作(DB CRUD / 文件处理) +│ └─ 使用 async def(Tornado Handler 默认)+ 直接调用同步代码 +│ +└─ 混合操作(同步 DB + 异步 Redis) + └─ 使用 async def,同步部分直接调用,异步部分 await +``` + +## Tornado 与 SQLAlchemy 的协作 + +本项目使用 **同步 SQLAlchemy**(scoped_session),在 Tornado 的 `async def` 中直接调用: + +```python +# src/modules/link/handlers.py +class LinkPageHandler(BaseHandler): + @permission_required("sys:link:list") + async def get(self): + # 直接调用同步 Service(内部使用同步 SQLAlchemy) + return await link_service.get_page(self) +``` + +```python +# src/modules/link/service.py +class LinkService(BaseService): + # 同步方法,直接调用 Repository + def get_page(self, handler): + # Repository 内部使用同步 SQLAlchemy Session + return self.repo.paginate(...) +``` + +:::warning 为什么同步 DB 操作在 Tornado 中可行? +本项目的同步 SQLAlchemy 操作通常执行很快(毫秒级),短暂阻塞 IOLoop 在实际业务中可接受。如果遇到慢查询场景,可考虑使用 `tornado.ioloop.IOLoop.current().run_in_executor()` 将其放入线程池。 +::: + +## Handler 方法的异步模式 + +### 标准模式:async def + +所有 Handler 方法默认使用 `async def`: + +```python +class LinkAddHandler(BaseHandler): + @permission_required("sys:link:add") + @check_demo + @operation_log("友链管理", "添加") + async def post(self): + return await link_service.add(self) +``` + +### 涉及 Redis 的同步操作 + +本项目使用 **同步 Redis 客户端**(`from core.redis import redis`),Redis 操作与 DB 操作一样是同步调用: + +```python +# src/modules/auth/service.py +from core.redis import redis + +async def login(self): + # 检查登录失败锁定(同步 Redis 操作) + lock_key = f"login:lock:{ip}:{username}" + is_locked = redis.get(lock_key) + if is_locked: + return R.failed(self, "账号已锁定,请稍后再试") + + # 验证用户名密码(同步 DB 操作) + user = UserRepository().get_by_username(username) + if not user: + # 记录失败次数(同步 Redis 操作) + redis.incr(f"login:fail:{ip}:{username}") + redis.expire(f"login:fail:{ip}:{username}", 300) + return R.failed(self, "用户名或密码错误") + + # 签发 JWT Token(同步操作) + token = create_token({"userId": user.id}) + + return R.ok(self, msg="登录成功", data={"access_token": token}) +``` + +## 各模块的异步使用模式 + +| 模块 | 模式 | 原因 | +|:---|:---|:---| +| Link / Position / Level | `async def` + 同步 DB | 纯 DB CRUD,同步操作快速返回 | +| Dept / Menu / Category | `async def` + 同步 DB | 纯 DB CRUD(含树形查询) | +| Role / User | `async def` + 同步 DB | 纯 DB CRUD(含关联查询) | +| Article / Notice | `async def` + 同步 DB + 文件 | DB CRUD + 文件处理 | +| Auth (login/logout) | `async def` + 混合 | 同步 DB + 异步 Redis | +| Dict / Config | `async def` + 同步 DB | DB CRUD + Redis 缓存 | +| Job (scheduler) | 同步 `def` | APScheduler 在独立线程执行 | + +## IOLoop 启动与关闭 + +### 启动流程 + +```python +# src/bootstrap.py +def main(): + app = create_app() + http_server = tornado.httpserver.HTTPServer(app) + http_server.listen(options.port) + + # 启动定时任务调度器 + from modules.job.scheduler import init_scheduler + init_scheduler() + + ioloop = tornado.ioloop.IOLoop.current() + + # 注册信号处理(优雅关闭) + for sig in (signal.SIGINT, signal.SIGTERM): + signal.signal(sig, lambda s, f: ioloop.add_callback_from_signal(_shutdown)) + + ioloop.start() +``` + +### 优雅关闭 + +```python +def _shutdown(): + """收到退出信号时关闭调度器并停止事件循环。""" + from modules.job.scheduler import shutdown_scheduler + shutdown_scheduler() + ioloop.stop() +``` + +## 禁止的模式 + +:::danger 避免在 Handler 中执行长时间同步操作 +在 `async def` 中执行长时间同步操作(如大文件读写、复杂计算)会阻塞整个 IOLoop,导致所有并发请求被阻塞。 +::: + +### 错误示例 + +```python +# 错误!在 async def 中执行长时间同步操作 +async def get(self): + # 大文件读取阻塞 IOLoop + with open("huge_file.csv", "r") as f: + data = f.read() # 阻塞! + self.finish({"code": 0, "data": data}) +``` + +### 正确示例 + +```python +# 正确:使用 run_in_executor 将阻塞操作放入线程池 +async def get(self): + import asyncio + loop = asyncio.get_event_loop() + data = await loop.run_in_executor(None, self._read_file, "huge_file.csv") + self.finish({"code": 0, "data": data}) + +def _read_file(self, path): + with open(path, "r") as f: + return f.read() +``` + +## 总结 + +Tornado 基于单线程 IOLoop 事件循环,Handler 方法默认 `async def`。本项目使用同步 SQLAlchemy,DB 操作通常毫秒级,直接在协程中调用可接受。涉及 Redis 等异步操作时使用 `await`。避免在协程中执行长时间同步操作,必要时使用 `run_in_executor` 放入线程池。定时任务调度器在独立线程运行,不受 IOLoop 影响。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/api-cheatsheet.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/api-cheatsheet.md new file mode 100644 index 0000000..1bc4b06 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/api-cheatsheet.md @@ -0,0 +1,278 @@ +:::tip 本章概要 +API 接口速查表,按模块分组列出所有 RESTful 接口的请求方法、路径、说明和权限标识。 +::: + +# API 接口速查表 + +本章汇总项目所有 API 接口,按模块分类整理。所有接口统一挂在 `/api/v1` 前缀下,需携带 `Authorization: Bearer ` 请求头(认证相关接口除外)。 + +## 认证管理(免登录) + +| 方法 | 路径 | 说明 | 权限 | +|:----|:----|:----|:----| +| GET | `/api/v1/captcha` | 获取图形验证码 | 无需登录 | +| POST | `/api/v1/login` | 用户登录 | 无需登录 | +| GET | `/api/v1/logout` | 退出登录 | 需登录 | + +## 用户管理 + +| 方法 | 路径 | 说明 | 权限 | +|:----|:----|:----|:----| +| GET | `/api/v1/user/page` | 分页查询 | `sys:user:page` | +| GET | `/api/v1/user/detail/{id}` | 详情查询 | `sys:user:detail` | +| GET | `/api/v1/user/list` | 列表查询 | `sys:user:list` | +| POST | `/api/v1/user/add` | 新增用户 | `sys:user:add` | +| PUT | `/api/v1/user/update` | 更新用户 | `sys:user:update` | +| DELETE | `/api/v1/user/delete/{id}` | 删除用户 | `sys:user:delete` | +| DELETE | `/api/v1/user/batchDelete` | 批量删除 | `sys:user:batchDelete` | +| PUT | `/api/v1/user/status` | 设置状态 | `sys:user:status` | +| PUT | `/api/v1/user/resetPwd` | 重置密码 | `sys:user:resetPwd` | +| POST | `/api/v1/user/import` | 导入用户 | `sys:user:import` | +| GET | `/api/v1/user/export` | 导出用户 | `sys:user:export` | + +## 角色管理 + +| 方法 | 路径 | 说明 | 权限 | +|:----|:----|:----|:----| +| GET | `/api/v1/role/page` | 分页查询 | `sys:role:page` | +| GET | `/api/v1/role/detail/{id}` | 详情查询 | `sys:role:detail` | +| GET | `/api/v1/role/list` | 列表查询 | `sys:role:list` | +| POST | `/api/v1/role/add` | 新增角色 | `sys:role:add` | +| PUT | `/api/v1/role/update` | 更新角色 | `sys:role:update` | +| DELETE | `/api/v1/role/delete/{id}` | 删除角色 | `sys:role:delete` | +| DELETE | `/api/v1/role/batchDelete` | 批量删除 | `sys:role:batchDelete` | +| PUT | `/api/v1/role/status` | 设置状态 | `sys:role:status` | +| GET | `/api/v1/role/roleMenu/{id}` | 获取角色菜单 | `sys:role:roleMenu` | +| POST | `/api/v1/role/assignMenu` | 分配菜单 | `sys:role:assignMenu` | + +## 菜单管理 + +| 方法 | 路径 | 说明 | 权限 | +|:----|:----|:----|:----| +| GET | `/api/v1/menu/list` | 菜单列表 | `sys:menu:list` | +| GET | `/api/v1/menu/detail/{id}` | 详情查询 | `sys:menu:detail` | +| POST | `/api/v1/menu/add` | 新增菜单 | `sys:menu:add` | +| PUT | `/api/v1/menu/update` | 更新菜单 | `sys:menu:update` | +| DELETE | `/api/v1/menu/delete/{id}` | 删除菜单 | `sys:menu:delete` | + +## 部门管理 + +| 方法 | 路径 | 说明 | 权限 | +|:----|:----|:----|:----| +| GET | `/api/v1/dept/list` | 部门列表 | `sys:dept:list` | +| GET | `/api/v1/dept/detail/{id}` | 详情查询 | `sys:dept:detail` | +| POST | `/api/v1/dept/add` | 新增部门 | `sys:dept:add` | +| PUT | `/api/v1/dept/update` | 更新部门 | `sys:dept:update` | +| DELETE | `/api/v1/dept/delete/{id}` | 删除部门 | `sys:dept:delete` | + +## 岗位管理 + +| 方法 | 路径 | 说明 | 权限 | +|:----|:----|:----|:----| +| GET | `/api/v1/position/page` | 分页查询 | `sys:position:page` | +| GET | `/api/v1/position/detail/{id}` | 详情查询 | `sys:position:detail` | +| GET | `/api/v1/position/list` | 列表查询 | `sys:position:list` | +| POST | `/api/v1/position/add` | 新增岗位 | `sys:position:add` | +| PUT | `/api/v1/position/update` | 更新岗位 | `sys:position:update` | +| DELETE | `/api/v1/position/delete/{id}` | 删除岗位 | `sys:position:delete` | +| DELETE | `/api/v1/position/batchDelete` | 批量删除 | `sys:position:batchDelete` | +| PUT | `/api/v1/position/status` | 设置状态 | `sys:position:status` | + +## 职级管理 + +| 方法 | 路径 | 说明 | 权限 | +|:----|:----|:----|:----| +| GET | `/api/v1/level/page` | 分页查询 | `sys:level:page` | +| GET | `/api/v1/level/detail/{id}` | 详情查询 | `sys:level:detail` | +| GET | `/api/v1/level/list` | 列表查询 | `sys:level:list` | +| POST | `/api/v1/level/add` | 新增职级 | `sys:level:add` | +| PUT | `/api/v1/level/update` | 更新职级 | `sys:level:update` | +| DELETE | `/api/v1/level/delete/{id}` | 删除职级 | `sys:level:delete` | +| DELETE | `/api/v1/level/batchDelete` | 批量删除 | `sys:level:batchDelete` | +| PUT | `/api/v1/level/status` | 设置状态 | `sys:level:status` | + +## 文章管理 + +| 方法 | 路径 | 说明 | 权限 | +|:----|:----|:----|:----| +| GET | `/api/v1/article/page` | 分页查询 | `sys:article:page` | +| GET | `/api/v1/article/detail/{id}` | 详情查询 | `sys:article:detail` | +| POST | `/api/v1/article/add` | 新增文章 | `sys:article:add` | +| PUT | `/api/v1/article/update` | 更新文章 | `sys:article:update` | +| DELETE | `/api/v1/article/delete/{id}` | 删除文章 | `sys:article:delete` | +| DELETE | `/api/v1/article/batchDelete` | 批量删除 | `sys:article:batchDelete` | +| PUT | `/api/v1/article/status` | 设置状态 | `sys:article:status` | + +## 分类管理 + +| 方法 | 路径 | 说明 | 权限 | +|:----|:----|:----|:----| +| GET | `/api/v1/category/list` | 分类列表 | `sys:category:list` | +| GET | `/api/v1/category/detail/{id}` | 详情查询 | `sys:category:detail` | +| POST | `/api/v1/category/add` | 新增分类 | `sys:category:add` | +| PUT | `/api/v1/category/update` | 更新分类 | `sys:category:update` | +| DELETE | `/api/v1/category/delete/{id}` | 删除分类 | `sys:category:delete` | + +## 公告管理 + +| 方法 | 路径 | 说明 | 权限 | +|:----|:----|:----|:----| +| GET | `/api/v1/notice/page` | 分页查询 | `sys:notice:page` | +| GET | `/api/v1/notice/detail/{id}` | 详情查询 | `sys:notice:detail` | +| POST | `/api/v1/notice/add` | 新增公告 | `sys:notice:add` | +| PUT | `/api/v1/notice/update` | 更新公告 | `sys:notice:update` | +| DELETE | `/api/v1/notice/delete/{id}` | 删除公告 | `sys:notice:delete` | +| DELETE | `/api/v1/notice/batchDelete` | 批量删除 | `sys:notice:batchDelete` | + +## 友链管理 + +| 方法 | 路径 | 说明 | 权限 | +|:----|:----|:----|:----| +| GET | `/api/v1/link/page` | 分页查询 | `sys:link:page` | +| GET | `/api/v1/link/detail/{id}` | 详情查询 | `sys:link:detail` | +| POST | `/api/v1/link/add` | 新增友链 | `sys:link:add` | +| PUT | `/api/v1/link/update` | 更新友链 | `sys:link:update` | +| DELETE | `/api/v1/link/delete/{id}` | 删除友链 | `sys:link:delete` | +| DELETE | `/api/v1/link/batchDelete` | 批量删除 | `sys:link:batchDelete` | + +## 参数管理 + +| 方法 | 路径 | 说明 | 权限 | +|:----|:----|:----|:----| +| GET | `/api/v1/param/page` | 分页查询 | `sys:param:page` | +| GET | `/api/v1/param/detail/{id}` | 详情查询 | `sys:param:detail` | +| POST | `/api/v1/param/add` | 新增参数 | `sys:param:add` | +| PUT | `/api/v1/param/update` | 更新参数 | `sys:param:update` | +| DELETE | `/api/v1/param/delete/{id}` | 删除参数 | `sys:param:delete` | +| DELETE | `/api/v1/param/batchDelete` | 批量删除 | `sys:param:batchDelete` | + +## 字典管理 + +| 方法 | 路径 | 说明 | 权限 | +|:----|:----|:----|:----| +| GET | `/api/v1/dict/page` | 字典分页查询 | `sys:dict:page` | +| GET | `/api/v1/dict/detail/{id}` | 字典详情 | `sys:dict:detail` | +| POST | `/api/v1/dict/add` | 新增字典 | `sys:dict:add` | +| PUT | `/api/v1/dict/update` | 更新字典 | `sys:dict:update` | +| DELETE | `/api/v1/dict/delete/{id}` | 删除字典 | `sys:dict:delete` | +| GET | `/api/v1/dictItem/page` | 字典项分页 | `sys:dictItem:page` | +| GET | `/api/v1/dictItem/detail/{id}` | 字典项详情 | `sys:dictItem:detail` | +| POST | `/api/v1/dictItem/add` | 新增字典项 | `sys:dictItem:add` | +| PUT | `/api/v1/dictItem/update` | 更新字典项 | `sys:dictItem:update` | +| DELETE | `/api/v1/dictItem/delete/{id}` | 删除字典项 | `sys:dictItem:delete` | + +## 配置管理 + +| 方法 | 路径 | 说明 | 权限 | +|:----|:----|:----|:----| +| GET | `/api/v1/config/page` | 配置分页查询 | `sys:config:page` | +| GET | `/api/v1/config/detail/{id}` | 配置详情 | `sys:config:detail` | +| POST | `/api/v1/config/add` | 新增配置 | `sys:config:add` | +| PUT | `/api/v1/config/update` | 更新配置 | `sys:config:update` | +| DELETE | `/api/v1/config/delete/{id}` | 删除配置 | `sys:config:delete` | +| GET | `/api/v1/configItem/page` | 配置项分页 | `sys:configItem:page` | +| GET | `/api/v1/configItem/detail/{id}` | 配置项详情 | `sys:configItem:detail` | +| POST | `/api/v1/configItem/add` | 新增配置项 | `sys:configItem:add` | +| PUT | `/api/v1/configItem/update` | 更新配置项 | `sys:configItem:update` | +| DELETE | `/api/v1/configItem/delete/{id}` | 删除配置项 | `sys:configItem:delete` | + +## 定时任务 + +| 方法 | 路径 | 说明 | 权限 | +|:----|:----|:----|:----| +| GET | `/api/v1/job/page` | 分页查询 | `sys:job:page` | +| GET | `/api/v1/job/detail/{id}` | 详情查询 | `sys:job:detail` | +| POST | `/api/v1/job/add` | 新增任务 | `sys:job:add` | +| PUT | `/api/v1/job/update` | 更新任务 | `sys:job:update` | +| DELETE | `/api/v1/job/delete/{id}` | 删除任务 | `sys:job:delete` | +| PUT | `/api/v1/job/status` | 设置状态 | `sys:job:status` | +| POST | `/api/v1/job/run` | 立即执行 | `sys:job:run` | +| GET | `/api/v1/jobLog/page` | 任务日志分页 | `sys:jobLog:page` | +| DELETE | `/api/v1/jobLog/clean` | 清空日志 | `sys:jobLog:clean` | + +## 文件上传 + +| 方法 | 路径 | 说明 | 权限 | +|:----|:----|:----|:----| +| POST | `/api/v1/upload/uploadFile` | 上传文件 | `sys:upload:file` | + +## 日志管理 + +| 方法 | 路径 | 说明 | 权限 | +|:----|:----|:----|:----| +| GET | `/api/v1/loginLog/page` | 登录日志分页 | `sys:loginLog:page` | +| DELETE | `/api/v1/loginLog/clean` | 清空登录日志 | `sys:loginLog:clean` | +| GET | `/api/v1/operationLog/page` | 操作日志分页 | `sys:operationLog:page` | +| DELETE | `/api/v1/operationLog/clean` | 清空操作日志 | `sys:operationLog:clean` | + +## 代码生成 + +| 方法 | 路径 | 说明 | 权限 | +|:----|:----|:----|:----| +| GET | `/api/v1/generator/page` | 数据库表分页 | `sys:generator:page` | +| GET | `/api/v1/generator/detail/{table_name}` | 表详情 | `sys:generator:detail` | +| GET | `/api/v1/generator/columns/{table_name}` | 表字段信息 | `sys:generator:detail` | +| POST | `/api/v1/generator/generate` | 代码生成 | `sys:generator:generate` | +| POST | `/api/v1/generator/batchGenerate` | 批量生成 | `sys:generator:batchGenerate` | + +## 公共接口 + +| 方法 | 路径 | 说明 | 权限 | +|:----|:----|:----|:----| +| GET | `/api/v1/index` | 首页数据 | 需登录 | + +## 通用请求格式 + +### 分页查询参数 + +| 参数 | 类型 | 默认值 | 说明 | +|:----|:----|:----|:----| +| `pageNo` | int | 1 | 页码 | +| `pageSize` | int | 10 | 每页条数 | +| 其他字段 | - | - | 按模块定义的筛选字段传递 | + +### 通用响应格式 + +```json +// 成功 +{ + "code": 0, + "msg": "操作成功", + "data": {}, + "ok": true +} + +// 分页响应 +{ + "code": 0, + "msg": "操作成功", + "data": { + "records": [], + "total": 100, + "size": 10, + "current": 1, + "pages": 10 + }, + "ok": true +} + +// 失败 +{ + "code": 1, + "msg": "错误信息", + "data": null, + "ok": false +} +``` + +### 批量删除请求体 + +```json +[1, 2, 3] +``` + +请求体为 ID 数组,Content-Type 为 `application/json`。 + +## 总结 + +API 接口速查表汇总了系统所有 RESTful 接口,按模块分组列出请求方法、路径、说明和权限标识。接口路径统一以 `/api/v1/` 为前缀,权限标识遵循 `sys:{模块}:{操作}` 命名规范。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/business/article.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/business/article.md new file mode 100644 index 0000000..a29edc0 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/business/article.md @@ -0,0 +1,436 @@ +:::tip 文章管理 +文章管理模块支持富文本编辑、分类关联、封面图片上传、图集管理等功能。适用于新闻、公告、帮助文档等内容管理场景。本模块是典型的「带文件处理」业务模块,涵盖了封面迁移、图集处理、富文本 XSS 清洗等核心机制。 +::: + +## 模块结构 + +``` +src/modules/article/ +├── models.py # 文章模型 +├── schemas.py # 表单验证 +├── repository.py # 数据访问层 +└── service.py # 业务逻辑层(自定义,非 BaseService 子类) +``` + +:::warning 自定义 Service +文章模块因涉及文件处理、富文本清洗、图集解析等复杂逻辑,未继承 `BaseService`,而是编写自定义 service 函数。这是项目中复杂模块的典型写法,与简单模块(如 Param 继承 BaseService)形成对比。 +::: + +## 文章模型 + +```python +# src/modules/article/models.py +class Article(BaseModel): + """文章模型类""" + __tablename__ = DB_PREFIX + "article" + __table_comment__ = "文章表" + + # 分类ID + category_id = Column(Integer, nullable=True, index=True, comment="分类ID") + # 文章标题 + title = Column(String(150), nullable=True, index=True, comment="文章标题") + # 文章封面(存相对路径,如 /article/20260907/xxx.png) + cover = Column(String(255), nullable=True, comment="文章封面") + # 文章图集(逗号分隔的相对路径,如 /article/20260907/a.png,/article/20260907/b.png) + images = Column(Text, nullable=True, comment="文章图集") + # 文章导读 + intro = Column(String(255), nullable=True, comment="文章导读") + # 文章内容(富文本 HTML,图片地址存为 [IMG_URL] 占位符 + 相对路径) + content = Column(Text, nullable=True, comment="文章内容") + # 文章作者 + author = Column(String(100), nullable=True, comment="文章作者") + # 状态:0-正常 1-下架 + status = Column(Integer, default=0, server_default=text('0'), nullable=False, index=True, + comment="文章状态:0-正常 1-下架") + # 点击量 + click = Column(Integer, default=0, server_default=text('0'), nullable=False, comment="文章点击率") + # 排序 + sort = Column(Integer, default=0, server_default=text('0'), nullable=False, comment="文章排序") +``` + +其中 `id`、`create_user`、`create_time`、`update_user`、`update_time`、`is_delete` 由基类 `base_model` 自动提供。 + +## 表单验证 + +### ArticleForm(新增/编辑) + +```python +class ArticleForm(BaseSchema): + categoryId: Optional[int] = Field(None, ge=0, description="分类ID") + title: str = Field(..., min_length=1, max_length=150, description="文章标题") + cover: Optional[str] = Field(None, max_length=255, description="文章封面") + images: Optional[str] = Field(None, description="文章图集") + intro: Optional[str] = Field(None, max_length=255, description="文章导读") + content: Optional[str] = Field(None, description="文章内容") + author: Optional[str] = Field(None, max_length=100, description="文章作者") + status: int = Field(0, ge=0, le=1, description="文章状态:0-正常 1-下架") + click: int = Field(0, ge=0, le=2147483647, description="文章点击率") + sort: int = Field(0, ge=0, le=99999, description="文章排序") +``` + +| 字段 | 必填 | 约束 | 说明 | +|:----|:----|:----|:----| +| `categoryId` | 否 | `ge=0` | 关联分类表,0 或不传表示无分类 | +| `title` | 是 | `1~150 字符` | 文章标题,唯一业务标识 | +| `cover` | 否 | `max_length=255` | 封面图片的临时 URL 或相对路径 | +| `images` | 否 | 无长度限制 | 图集,逗号分隔的 URL 字符串 | +| `intro` | 否 | `max_length=255` | 文章导读/摘要 | +| `content` | 否 | 无长度限制 | 富文本 HTML 内容 | +| `author` | 否 | `max_length=100` | 文章作者 | +| `status` | 否 | `0~1` | 默认 0(正常),1 为下架 | +| `click` | 否 | `0~2147483647` | 点击量,前端通常不传 | +| `sort` | 否 | `0~99999` | 排序号,值越大越靠前 | + +### ArticleStatusForm(状态变更) + +```python +class ArticleStatusForm(BaseModel): + id: int = Field(..., gt=0, description="文章ID") + status: int = Field(..., ge=0, le=1, description="文章状态:0-正常 1-下架") +``` + +用于独立的状态变更接口,只需传 `id` 和 `status`。 + +## 文件处理机制 + +文章模块涉及三类文件字段,处理方式各不相同: + +### 处理流程总览 + +``` +前端上传临时文件 → 获得临时 URL(如 /temp/20260907/xxx.png) + ↓ +提交表单(cover / images / content 携带临时 URL) + ↓ +后端 service 调用 save_file / save_content + ↓ +临时文件迁移到正式目录(如 /article/20260907/xxx.png) + ↓ +数据库存相对路径(不存完整 URL) + ↓ +查询时调用 get_file_url 拼接完整访问地址 +``` + +### 1. 封面图片(cover)— `save_file` + +```python +# 新增时 +cover = save_file(data.cover, "article") if data.cover else None +``` + +`save_file(url, directory)` 的处理逻辑: +1. 将前端传来的 URL 还原为磁盘绝对路径 +2. 若文件在临时目录(`temp/`),移动到正式目录(`upload/article/日期/`) +3. 若文件已在正式目录,直接返回相对路径 +4. 返回相对路径存入数据库(如 `/article/20260907/xxx.png`) +5. 远程 URL(非本站文件)不处理,返回空字符串 + +:::warning 防路径穿越 +`save_file` 内部会校验磁盘真实路径必须位于 `UPLOAD_DIR` 内,防止 `../` 路径穿越攻击。 +::: + +### 2. 图集(images)— `_build_images` / `_parse_images` + +图集在数据库中存储为**逗号分隔的相对路径字符串**: + +``` +/article/20260907/a.png,/article/20260907/b.png,/article/20260907/c.png +``` + +**写入时**(`_build_images`): + +```python +def _build_images(images): + """逗号分隔的临时 URL → 逐个 save_file 迁移 → 逗号分隔的相对路径""" + if not images: + return None + saved = [] + for url in images.split(','): + url = url.strip() + if not url: + continue + img = save_file(url, "article") + saved.append(img or url) + return ','.join(saved) +``` + +**读取时**(`_parse_images`): + +```python +def _parse_images(images): + """逗号分隔的相对路径 → 逐个 get_file_url 拼接 → 完整 URL 数组""" + if not images: + return [] + return [get_file_url(img) for img in images.split(',') if img] +``` + +前端拿到的是 URL 数组,可直接用于 `` 渲染。 + +### 3. 富文本内容(content)— `save_content` + +```python +# 新增时 +content = save_content(data.content, data.title, "article") if data.content else None +``` + +`save_content(content, title, directory)` 的处理流程: + +``` +原始 HTML + ↓ +① bleach 白名单清洗(移除 +``` + +### 方式 2:表格列显示字典文本 + +在 `columns.ts` 中使用 `render` 函数: + +```typescript +import { h } from 'vue'; +import { ElTag } from 'element-plus'; + +export const columns = [ + { + label: '培训方式', + prop: 'trainingMethod', + render(record) { + const map = { 1: '线上', 2: '线下', 3: '混合' }; + const typeMap = { 1: 'success', 2: 'warning', 3: 'info' }; + return h(ElTag, { type: typeMap[record.row.trainingMethod] }, { + default: () => map[record.row.trainingMethod] || '-' + }); + }, + }, +]; +``` + +### 方式 3:搜索表单下拉 + +在 `querySchemas.ts` 中配置: + +```typescript +export const schemas: FormSchema[] = [ + { + field: 'trainingMethod', + component: 'Select', + label: '培训方式', + componentProps: { + placeholder: '请选择培训方式', + clearable: true, + options: [ + { label: '线上', value: '1' }, + { label: '线下', value: '2' }, + { label: '混合', value: '3' }, + ], + }, + }, +]; +``` + +## 代码生成器与字典 + +代码生成器会自动识别字段注释中的枚举格式(`1-线上 2-线下 3-混合`),并: + +1. 自动解析 `choices` 列表 +2. 自动生成 `dict_code`(格式:`{table_name}_{field_name}`) +3. 自动配置 `serialize_maps` +4. 前端自动生成下拉选择框 + +生成后需在后台手动创建对应的字典类型和字典项。 + +## 字典缓存 + +项目使用进程内缓存 + Redis 二级缓存存储字典数据: + +| 层级 | 说明 | 失效策略 | +|:----|:----|:----| +| L1 进程内缓存 | Python 字典,TTL 60 秒 | 自动过期 | +| L2 Redis 缓存 | Redis 字符串,TTL 120 秒 | 自动过期 + 主动失效 | + +字典数据修改时自动清除相关缓存。 + +## 字典管理 API + +| 方法 | 路径 | 说明 | +|:----|:----|:----| +| GET | `/api/v1/dict/page` | 字典类型分页 | +| POST | `/api/v1/dict/add` | 新增字典类型 | +| PUT | `/api/v1/dict/update` | 更新字典类型 | +| DELETE | `/api/v1/dict/delete/{id}` | 删除字典类型 | +| GET | `/api/v1/dictItem/page` | 字典项分页 | +| POST | `/api/v1/dictItem/add` | 新增字典项 | +| PUT | `/api/v1/dictItem/update` | 更新字典项 | +| DELETE | `/api/v1/dictItem/delete/{id}` | 删除字典项 | + +## 最佳实践 + +1. **字典编码命名**:使用 `模块_字段` 格式,如 `user_status`、`article_type` +2. **字典值使用整数**:便于排序和比较,避免使用字符串 +3. **字典标签简洁**:控制在 4 个字以内,适合表格列显示 +4. **状态字段统一**:`1-正常 2-停用` 全项目保持一致 +5. **生成后补充字典**:代码生成器创建的 `dict_code` 需在后台手动创建对应数据 + +## 总结 + +数据字典通过「字典类型 + 字典项」两层结构管理枚举选项。后端通过 `serialize_maps` 自动转换,前端通过 API 获取下拉数据。代码生成器会自动识别注释中的枚举格式并生成对应配置。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/core/dict.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/core/dict.md new file mode 100644 index 0000000..2ba8d18 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/core/dict.md @@ -0,0 +1,209 @@ +:::tip 数据字典 +数据字典用于管理系统中的枚举值和配置项,如状态、类型、性别等。通过字典编码统一管理,前端可通过 `useDictStore` 获取字典选项,避免硬编码。后端读取工具位于 `src/common/utils/dict_util.py`,内置进程内 TTL 缓存。 +::: + +## 模块结构 + +``` +src/modules/dictionary/ +├── dict/ # 字典类型(分组) +│ ├── models.py +│ ├── schemas.py +│ ├── repository.py +│ └── service.py +└── dict_item/ # 字典项(具体值) + ├── models.py + ├── schemas.py + ├── repository.py + └── service.py +``` + +## 字典模型 + +### 字典类型(Dict) + +```python +# src/modules/dictionary/dict/models.py +# ============================================================ +# 字典模型 +# ============================================================ +class Dict(BaseModel): + """字典模型类""" + # ============================================================ + # 表名配置 + # ============================================================ + __tablename__ = DB_PREFIX + "dict" + __table_comment__ = "数据字典表" + + # ============================================================ + # 字段定义 + # ============================================================ + # 字典名称 + name = Column(String(150), nullable=False, index=True, comment="字典名称") + # 字典编码 + code = Column(String(150), nullable=False, index=True, comment="字典编码") + # 字典排序 + sort = Column(Integer, default=0, server_default=text('0'), comment="字典排序") + # 字典备注 + note = Column(String(255), nullable=True, comment="字典备注") + + # ============================================================ + # 内置方法 + # ============================================================ + def __str__(self): + """返回字典ID作为字符串表示""" + return "字典{}".format(self.id) +``` + +### 字典项(DictItem) + +```python +# src/modules/dictionary/dict_item/models.py +# ============================================================ +# 字典项模型 +# ============================================================ +class DictItem(BaseModel): + """字典项模型类""" + # ============================================================ + # 表名配置 + # ============================================================ + __tablename__ = DB_PREFIX + "dict_item" + __table_comment__ = "字典项表" + + # ============================================================ + # 字段定义 + # ============================================================ + # 字典项名称 + name = Column(String(150), nullable=False, index=True, comment="字典项名称") + # 字典项值 + value = Column(String(150), nullable=False, comment="字典项值") + # 字典ID + dict_id = Column(Integer, default=0, server_default=text('0'), index=True, comment="字典ID") + # 字典项排序 + sort = Column(Integer, default=0, server_default=text('0'), comment="字典项顺序") + # 字典项备注 + note = Column(String(255), nullable=True, comment="字典项备注") + + # ============================================================ + # 内置方法 + # ============================================================ + def __str__(self): + """返回字典项ID作为字符串表示""" + return "字典项{}".format(self.id) +``` + +## 使用示例 + +### 后端获取字典(推荐使用 dict_util) + +```python +from utils.dict_util import get_dict_items, get_dict_map, get_dict_label + +# 获取字典项列表(自动缓存,TTL 60秒兜底) +items = get_dict_items("sys_status") +# 返回: [{"label": "正常", "value": "1", "sort": 0}, {"label": "停用", "value": "2", "sort": 1}] + +# 获取 {value: label} 映射 +status_map = get_dict_map("sys_status") +# 返回: {"1": "正常", "2": "停用"} + +# 单值翻译(value 自动转字符串匹配) +label = get_dict_label("sys_status", 1, default="未知") +# 返回: "正常" +``` + +### dict_util 缓存工具详解 + +项目提供 `dict_util` 工具(`src/common/utils/dict_util.py`),内置进程内 TTL 缓存。由于字典数据几乎不变,且读写均落在同步 service 路径(无法使用请求级异步 Redis),因此采用进程内缓存而非 Redis 缓存: + +```python +from utils.dict_util import get_dict_items, get_dict_map, get_dict_label + +# 获取字典项列表(自动缓存,TTL 60秒兜底) +items = get_dict_items("sys_status") +# 返回: [{"label": "正常", "value": "1", "sort": 0}, {"label": "停用", "value": "2", "sort": 1}] + +# 获取 {value: label} 映射 +status_map = get_dict_map("sys_status") +# 返回: {"1": "正常", "2": "停用"} + +# 单值翻译(value 自动转字符串匹配) +label = get_dict_label("sys_status", 1, default="未知") +# 返回: "正常" +``` + +缓存实现细节: + +```python +# 进程内字典缓存:{code: (过期时间戳, 字典项列表)} +_dict_cache: dict = {} +_CACHE_TTL = 60 # 秒 + +def get_dict_items(code): + """按字典编码读取字典项列表,字典不存在返回空列表""" + now = time.time() + hit = _dict_cache.get(code) + if hit and hit[0] > now: + return hit[1] + items = _load_dict_items(code) + _dict_cache[code] = (now + _CACHE_TTL, items) + return items + +def invalidate_dict(code): + """清除指定字典编码的缓存(字典/字典项增删改时由 service 主动调用)""" + if code: + _dict_cache.pop(code, None) + +def invalidate_dicts_by_ids(dict_ids): + """按字典ID批量清除缓存:字典项变更后据 dict_id 反查 code 再失效""" +``` + +:::warning value 类型 +字典项的 `value` 字段为字符串类型,`get_dict_label()` 内部会自动将查询值转为字符串匹配。前端使用时注意类型转换。 +::: + +### 前端使用 + +前端通过 `useDictStore` 获取字典数据: + +```javascript +import { useDictStore } from '@/store/dict' + +const dictStore = useDictStore() + +// 获取字典选项 +const statusOptions = dictStore.getDict('sys_status') +// [{label: "正常", value: "1"}, {label: "停用", value: "2"}] + +// 在 el-select 中使用 + + + +``` + +## 常用字典示例 + +| 字典编码 | 说明 | 字典项 | +|---------|------|--------| +| `sys_status` | 系统状态 | 正常(1)、停用(2) | +| `sys_gender` | 性别 | 男(1)、女(2) | +| `operation_type` | 操作类型 | 新增(1)、修改(2)、删除(3) 等 | +| `menu_type` | 菜单类型 | 菜单(0)、节点(1) | + +## 总结 + +数据字典模块具备以下特点: + +``` +1. 两级结构:字典类型(分组)+ 字典项(具体值),清晰管理 +2. 进程内缓存:dict_util 内置 TTL 缓存(60秒兜底),变更时主动失效 +3. 前端集成:useDictStore 统一管理,el-select 直接绑定 +4. 值为字符串:字典项 value 统一为字符串,get_dict_label 自动转字符串匹配 +5. 避免硬编码:枚举值统一走字典,修改无需改代码 +6. 三种取值:get_dict_items(列表)、get_dict_map(映射)、get_dict_label(单值翻译) +``` diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/core/exception.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/core/exception.md new file mode 100644 index 0000000..6783f7e --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/core/exception.md @@ -0,0 +1,153 @@ +:::tip 全局异常与响应 +定义了统一的响应格式和全局异常处理机制。所有 API 返回 `{code, data, msg, ok}` 标准格式,异常被捕获后也转换为相同格式。响应函数位于 `src/core/response.py`,异常类型位于 `src/core/exceptions.py`。 +::: + +## 统一响应格式 + +`src/core/response.py` 提供统一的响应生成函数: + +```python +from core import response as R + +# 成功响应 +R.ok(data={"id": 1}, msg="操作成功") +# {"code": 0, "data": {"id": 1}, "msg": "操作成功", "ok": true} + +# 成功响应(带额外字段) +R.ok(data=list, count=total) +# {"code": 0, "data": [...], "msg": "操作成功", "ok": true, "count": 100} + +# 失败响应 +R.failed(msg="参数错误") +# {"code": 1, "data": null, "msg": "参数错误", "ok": false} +``` + +### 响应函数说明 + +| 函数 | 说明 | code | ok | +|------|------|------|------| +| `R.ok()` | 成功响应 | 0 | true | +| `R.failed()` | 失败响应 | 1 | false | +| `R.page()` | 分页响应 | 0 | true | +| `R.response()` | 通用响应 | 自定义 | 自定义 | + +函数签名: + +```python +def ok(self, data=None, msg="操作成功", code=0, **kwargs) -> None: + """生成成功响应,支持通过 kwargs 添加额外字段(如 count)""" + +def failed(self, msg="操作失败", code=1, data=None, **kwargs) -> None: + """生成失败响应""" + +def page(self, data, total, current, size, msg="操作成功", code=0) -> None: + """分页响应:封装 {records, total, size, current, pages} 标准分页结构""" + +def response(self, data=None, msg="操作成功", code=0, success=True, **kwargs) -> None: + """通用响应生成器(高级用法),允许完全自定义字段""" +``` + +### 分页响应 + +```python +R.page(data=records, total=100, current=1, size=10) +# { +# "code": 0, +# "data": { +# "records": [...], +# "total": 100, +# "size": 10, +# "current": 1, +# "pages": 10 +# }, +# "msg": "操作成功", +# "ok": true +# } +``` + +## 异常类型 + +### AuthError + +认证异常,由 `login_required` 装饰器抛出: + +```python +# src/core/exceptions.py +class AuthError(BaseAppError): + code = 401 + status_code = 401 + msg = "登录过期" +``` + +### BusinessError + +业务异常,业务代码可主动抛出: + +```python +class BusinessError(BaseAppError): + msg = "操作失败" +``` + +使用示例: + +```python +from core.exceptions import BusinessError + +def some_business_logic(): + if some_condition: + raise BusinessError(msg="数据不存在") +``` + +## 全局异常处理 + +`BaseHandler.write_error()` 统一捕获异常并转换为标准格式: + +```python +# src/core/base_handler.py +class BaseHandler(tornado.web.RequestHandler): + def write_error(self, status_code, **kwargs): + exc = kwargs.get("exc_info", (None, None, None))[1] + if isinstance(exc, BaseAppError): + # 业务异常:按类型分级记录日志 + if isinstance(exc, (BusinessError, ValidationError, NotFoundError)): + logger.warning("业务异常 path=%s msg=%s", self.request.path, exc.msg) + else: + logger.error("请求异常 path=%s msg=%s", self.request.path, exc.msg) + self.set_status(exc.status_code) + self.finish({"code": exc.code, "data": None, "msg": exc.msg, "ok": False}) + else: + # 未预期异常:记录完整堆栈 + logger.error("未捕获异常 path=%s", self.request.path, exc_info=exc_info) + self.set_status(500) + self.finish({"code": 500, "data": None, "msg": "服务器内部错误", "ok": False}) +``` + +:::tip 异常处理机制 +Tornado 通过 `write_error()` 方法统一处理异常。`BaseAppError` 及其子类会被捕获并转换为标准响应格式,未预期异常返回 500 错误码。 +::: + +## 状态码常量 + +```python +# src/core/response.py +class Codes: + SUCCESS = 0 + FAILED = 1 + UNAUTHORIZED = 401 # 未认证 + FORBIDDEN = 403 # 无权限 + NOT_FOUND = 404 # 资源不存在 + VALIDATE_ERROR = 422 # 验证错误 + SERVER_ERROR = 500 # 服务器错误 +``` + +## 总结 + +全局异常与响应模块具备以下特点: + +``` +1. 统一格式:所有 API 返回 {code, data, msg, ok} 标准格式 +2. 全局捕获:认证异常、业务异常、HTTP异常、验证异常、未捕获异常 +3. HTTP 200:所有响应均返回 200,业务状态通过 code 区分 +4. 错误聚合:多个验证错误用 / 分隔,便于前端展示 +5. 安全隐藏:未捕获异常返回通用提示,不暴露内部错误信息 +``` diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/core/graceful-shutdown.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/core/graceful-shutdown.md new file mode 100644 index 0000000..79de05e --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/core/graceful-shutdown.md @@ -0,0 +1,111 @@ +# 优雅关闭 + +:::tip 说明 +优雅关闭确保应用在收到终止信号时,能够完成正在处理的请求、关闭数据库连接、停止定时任务调度器后再退出,避免请求中断和资源泄漏。 +::: + +## 工作原理 + +``` +收到 SIGINT / SIGTERM 信号 + │ + ├─ 停止接收新请求 + ├─ 关闭定时任务调度器(shutdown_scheduler) + ├─ 停止 IOLoop 事件循环 + └─ 进程退出 +``` + +## 实现 + +```python +# src/bootstrap.py +import signal +import tornado.httpserver +import tornado.ioloop + +def main(): + app = create_app() + http_server = tornado.httpserver.HTTPServer(app) + http_server.listen(options.port) + + # 启动定时任务调度器 + from modules.job.scheduler import init_scheduler + init_scheduler() + + ioloop = tornado.ioloop.IOLoop.current() + + # ============================================================ + # 优雅关闭回调 + # ============================================================ + def _shutdown(): + """收到退出信号时关闭调度器并停止事件循环。""" + logger.info("收到退出信号,正在优雅关闭...") + from modules.job.scheduler import shutdown_scheduler + shutdown_scheduler() + ioloop.stop() + + # ============================================================ + # 注册信号处理(优雅关闭) + # ============================================================ + for sig in (signal.SIGINT, signal.SIGTERM): + try: + signal.signal(sig, lambda s, f: ioloop.add_callback_from_signal(_shutdown)) + except (ValueError, AttributeError, OSError): + pass + + ioloop.start() +``` + +## 信号说明 + +| 信号 | 触发方式 | 说明 | +|:-----|:---------|:-----| +| `SIGINT` | Ctrl+C | 终端中断信号 | +| `SIGTERM` | `kill ` | 进程终止信号(Docker/Supervisor 默认发送) | + +:::warning Windows 兼容 +Windows 下 `SIGTERM` 可能不完全支持,`SIGINT`(Ctrl+C)是主要的关闭方式。代码中使用 `try/except` 包裹信号注册,避免 Windows 下的兼容性问题。 +::: + +## 关闭顺序 + +1. **停止定时任务调度器**:`shutdown_scheduler()` 停止 APScheduler,等待正在执行的任务完成 +2. **停止 IOLoop**:`ioloop.stop()` 停止事件循环,进程自然退出 +3. **BaseHandler.on_finish**:每个请求结束后自动关闭 DB 会话(兜底释放) + +## Docker 中的使用 + +Docker 发送 `SIGTERM` 等待 30 秒后发送 `SIGKILL`: + +```dockerfile +# Dockerfile +STOPSIGNAL SIGTERM +``` + +```yaml +# docker-compose.yml +services: + app: + stop_grace_period: 30s +``` + +## Supervisor 中的使用 + +```ini +[program:djangoadmin] +command=poetry run python app.py --port=8041 +stopsignal=TERM +stopwaitsecs=30 +``` + +## 总结 + +优雅关闭方案具备以下特点: + +``` +1. 信号驱动:SIGINT / SIGTERM 触发关闭流程 +2. 调度器先停:先关闭定时任务,避免任务执行到一半 +3. IOLoop 停止:事件循环停止后进程自然退出 +4. DB 会话兜底:BaseHandler.on_finish 确保会话释放 +5. 容器友好:Docker / Supervisor 默认发送 SIGTERM +``` diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/core/jwt.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/core/jwt.md new file mode 100644 index 0000000..5b22053 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/core/jwt.md @@ -0,0 +1,407 @@ +:::tip JWT认证 +JWT(JSON Web Token)是的核心认证机制。用户登录成功后获取 Token,后续请求通过 `Authorization: Bearer ` 头携带 Token 进行身份验证。项目内置了 Token 生成、解析、刷新和黑名单管理功能,源码位于 `src/core/jwt.py`。 +::: + +## 认证流程 + +``` +1. 前端发送 POST /api/v1/login(username + password + captcha) +2. 后端校验验证码、用户名密码 +3. 登录成功 → create_token() 生成 JWT +4. 前端存储 Token,后续请求携带 Authorization: Bearer +5. login_required 装饰器解析 Token → 写入上下文(contextvars) +6. 退出登录 → Token 加入 Redis 黑名单立即失效 +``` + +## 配置项 + +| 环境变量 | 默认值 | 说明 | +|---------|--------|------| +| `JWT_SECRET` | 无(必填) | JWT 签名密钥,至少32字节 | +| `JWT_EXPIRE_MINUTES` | `20` | Token 过期时间(分钟) | + +:::warning 密钥安全 +`JWT_SECRET` 必须通过环境变量设置,启动时会校验密钥强度(至少32字节),强度不足将拒绝启动。生成方式: +```bash +python -c "import secrets; print(secrets.token_urlsafe(48))" +``` +::: + +## Token 生成 + +`src/core/jwt.py` 中的 `create_token()` 函数负责生成 JWT: + +```python +from core.jwt import create_token + +# 登录成功后生成 Token +access_token = create_token({ + "userId": user.id, + "username": user.username, + "realname": user.realname or '' +}) +``` + +函数签名与核心逻辑: + +```python +def create_token(payload, timeout=None): + """ + 生成JWT令牌 + + 参数: + payload (dict): JWT载荷数据,通常包含userId、username等信息 + timeout (int): 令牌过期时间(分钟),默认使用DEFAULT_TIMEOUT_MINUTES(来自JWT_EXPIRE_MINUTES) + + 返回: + str: 生成的JWT令牌字符串 + """ + if timeout is None: + timeout = DEFAULT_TIMEOUT_MINUTES + + payload_copy = payload.copy() + current_time = datetime.datetime.now(tz=datetime.timezone.utc) + payload_copy['exp'] = current_time + datetime.timedelta(minutes=timeout) + payload_copy['iat'] = current_time + + headers = {"typ": "JWT", "alg": JWT_ALGORITHM} + + token = jwt.encode( + payload=payload_copy, + key=JWT_SECRET, + algorithm=JWT_ALGORITHM, + headers=headers + ) + return token +``` + +Token 结构包含三部分: + +```python +# Header +{ + "typ": "JWT", + "alg": "HS256" +} + +# Payload(自动添加 exp 和 iat) +{ + "userId": 1, + "username": "admin", + "realname": "管理员", + "exp": "2026-09-05T12:00:00Z", # 过期时间 + "iat": "2026-09-05T11:40:00Z" # 签发时间 +} +``` + +## Token 解析验证 + +`parse_payload()` 函数验证 Token 签名和过期状态,返回标准格式结果: + +```python +from core.jwt import parse_payload + +result = parse_payload(token) +if result['code'] == 0: + user_id = result['data']['userId'] + username = result['data']['username'] +else: + error_msg = result['msg'] + # "token已失效,请重新登录" / "token认证失败" / "非法的token" +``` + +核心实现: + +```python +def parse_payload(token): + """解析验证JWT令牌,返回 {code, data, msg} 标准格式""" + result = {"code": 0, "data": None, "msg": "操作成功"} + + if not token: + result['code'] = 1 + result['msg'] = "token不能为空" + return result + + try: + verified_payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM]) + result['data'] = verified_payload + except exceptions.ExpiredSignatureError: + result['code'] = 1 + result['msg'] = "token已失效,请重新登录" + except exceptions.DecodeError: + result['code'] = 1 + result['msg'] = "token认证失败,无效的令牌格式" + except exceptions.InvalidTokenError: + result['code'] = 1 + result['msg'] = "非法的token,请检查令牌有效性" + + return result +``` + +异常处理说明: + +| 异常类型 | 提示信息 | 说明 | +|---------|---------|------| +| `ExpiredSignatureError` | token已失效,请重新登录 | Token 过期 | +| `DecodeError` | token认证失败,无效的令牌格式 | 格式错误或签名无效 | +| `InvalidTokenError` | 非法的token | 算法不匹配等 | + +## 登录接口 + +登录接口 `POST /api/v1/login` 接收 `LoginForm` 参数: + +```python +# src/modules/auth/schemas.py +class LoginForm(BaseModel): + username: str = Field(..., min_length=1, max_length=20, description="登录账号") + password: str = Field(..., min_length=6, max_length=128, description="登录密码") + code: str = Field(..., min_length=CAPTCHA_LENGTH, max_length=CAPTCHA_LENGTH, description="验证码") + key: str = Field(..., min_length=1, description="KEY值") +``` + +登录逻辑位于 `src/modules/auth/service.py`,完整流程包括锁定检查、验证码校验、密码比对、默认密码检测: + +```python +async def login(request, data: LoginForm): + ip = get_client_ip(request) + redis = get_redis() + + # 1. 检查是否被锁定(IP+用户名维度) + locked, remaining = await _is_locked(redis, ip, data.username) + if locked: + return R.failed(f"登录失败次数过多,请{remaining}秒后再试") + + # 2. 验证码校验(Lua 原子消费,防重放) + if not await verify_captcha(data.key, data.code): + return R.failed("验证码不正确") + + # 3. 查询用户(同步DB查询放入线程池) + user = await asyncio.to_thread( + lambda: user_repo.get_one(username=data.username, status=1) + ) + if not user: + await _record_fail(redis, ip, data.username) + return R.failed("用户名或密码不正确") + + # 4. 比对密码(bcrypt CPU密集,放入线程池) + password_ok = await asyncio.to_thread( + password.verify_password, data.password, user.salt or '', user.password or '') + if not password_ok: + await _record_fail(redis, ip, data.username) + return R.failed("用户名或密码不正确") + + # 5. 登录成功,清除失败记录 + await _clear_fail(redis, ip, data.username) + + # 6. 检测是否为默认密码(仅提示,不阻断登录) + is_default_pwd = bool(user.password and await asyncio.to_thread( + password.verify_password, DEFAULT_PASSWORD, user.salt or '', user.password or '')) + + # 7. 生成 Token + access_token = create_token({ + "userId": user.id, + "username": user.username, + "realname": user.realname or '' + }) + + return R.ok(msg="登录成功", data={ + "access_token": access_token, + "must_change_password": is_default_pwd + }) +``` + +登录成功返回: + +```json +{ + "code": 0, + "msg": "登录成功", + "data": { + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." + }, + "ok": true +} +``` + +## Token 刷新 + +Token 支持无感刷新,刷新时会执行令牌轮换(旧 Token 立即失效): + +```python +from core.jwt import refresh_token + +result = await refresh_token(old_token, extend_minutes=30) +if result['code'] == 0: + new_token = result['data']['access_token'] + expires_in = result['data']['expires_in'] # 秒 +``` + +刷新核心逻辑: + +```python +async def refresh_token(token, extend_minutes=None): + # 1. 验证原令牌有效性 + result = parse_payload(token) + if result['code'] != 0: + return {"code": 1, "data": None, "msg": f"令牌无效,无法刷新: {result['msg']}"} + + # 2. 已注销的令牌禁止刷新(防止攻击者用已注销 token 无限轮换) + if await is_token_blacklisted(token): + return {"code": 1, "data": None, "msg": "令牌已注销,无法刷新,请重新登录"} + + # 3. 移除JWT标准字段,保留业务数据 + payload = result['data'] + clean_payload = {k: v for k, v in payload.items() if k not in ['exp', 'iat', 'nbf']} + + # 4. 生成新令牌 + new_token = create_token(clean_payload, timeout=extend_minutes) + + # 5. 令牌轮换:将旧令牌加入黑名单 + await add_token_to_blacklist(token) + + return { + "code": 0, + "data": {"access_token": new_token, "expires_in": extend_minutes * 60}, + "msg": "令牌刷新成功" + } +``` + +:::tip 令牌轮换 +刷新后旧 Token 会被加入黑名单,防止旧 Token 在有效期内继续使用。已注销的 Token 不允许刷新,防止攻击者利用已注销令牌无限轮换出新令牌。 +::: + +## Token 黑名单 + +退出登录时将 Token 加入 Redis 黑名单,使其立即失效: + +```python +from core.jwt import add_token_to_blacklist, is_token_blacklisted + +# 退出登录:加入黑名单 +await add_token_to_blacklist(token) + +# 中间件校验:检查是否已注销 +if await is_token_blacklisted(token): + # 拒绝访问 +``` + +黑名单实现细节: + +```python +# 黑名单 Key 前缀 +_TOKEN_BLACKLIST_PREFIX = 'token:blacklist:' + +# SHA256 指纹存储,不将完整 Token 存入 Redis +def _token_fingerprint(token: str) -> str: + return hashlib.sha256(token.encode('utf-8')).hexdigest() + +async def add_token_to_blacklist(token: str) -> bool: + """退出登录时调用,TTL 与 Token 剩余有效期一致""" + expiration = get_token_expiration(token) + ttl_seconds = int((expiration - datetime.datetime.now(tz=datetime.timezone.utc)).total_seconds()) + fingerprint = _token_fingerprint(token) + key = f'{_TOKEN_BLACKLIST_PREFIX}{fingerprint}' + redis.set(key, '1', ex=ttl_seconds) + +async def is_token_blacklisted(token: str) -> bool: + """检查 Token 是否已注销,Redis 不可用时 fail-open 放行""" + try: + fingerprint = _token_fingerprint(token) + key = f'{_TOKEN_BLACKLIST_PREFIX}{fingerprint}' + return redis.exists(key) > 0 + except Exception as e: + # fail-open:Redis 故障时放行,避免全站不可用 + _log_blacklist_check_alert(e) + return False +``` + +黑名单特性: +- 使用 SHA256 指纹存储,不将完整 Token 存入 Redis +- TTL 与 Token 剩余有效期一致,过期自动清理 +- Redis 不可用时采用 fail-open 策略放行(避免全站不可用) +- 告警节流:Redis 故障期间首次失败记 ERROR,随后每 60 秒至多一条 ERROR,避免日志刷屏 + +## 请求中的 Token 提取 + +```python +from core.jwt import get_access_token, parse_token + +# 从请求头提取 Token +success, token, msg = get_access_token(request) + +# 完整解析(提取 + 验证) +result = parse_token(request) +# result['code'] == 0 表示成功 +# result['data']['userId'], result['data']['username'] +``` + +`get_access_token()` 支持 Bearer 前缀大小写不敏感(RFC 7235): + +```python +def get_access_token(request): + """从请求头中提取 access_token,返回 (success, token, message)""" + auth_header = request.headers.get('Authorization', '') + stripped = auth_header.strip() + if stripped.lower().startswith('bearer '): + access_token = stripped[7:].strip() + else: + access_token = stripped + return True, access_token, 'success' +``` + +## login_required 装饰器 + +`src/common/middleware/authentication.py` 中的 `login_required` 是全局认证装饰器,应用于 `BaseHandler.prepare()`: + +```python +# 认证成功后写入上下文变量(userId / username / realname 均从 JWT payload 读取) +current_user_id.set(int(data["userId"])) +current_username.set(data.get("username", "")) +current_realname.set(data.get("realname", "")) +``` + +业务代码通过 `core.context` 获取当前用户信息: + +```python +from core.context import current_user_id, current_username, current_realname + +user_id = current_user_id.get() # 用户 ID +username = current_username.get() # 用户名 +realname = current_realname.get() # 姓名 +``` + +## 前端集成 + +前端登录后将 Token 存入 localStorage,每次请求自动携带: + +```javascript +// 请求拦截器 +axios.interceptors.request.use(config => { + const token = localStorage.getItem('access_token') + if (token) { + config.headers['Authorization'] = `Bearer ${token}` + } + return config +}) + +// 响应拦截器:检测 Token 过期 +axios.interceptors.response.use(response => { + if (response.data.code === 401) { + // Token 过期,跳转登录页 + router.push('/login') + } + return response +}) +``` + +## 总结 + +的 JWT 认证方案具备以下特点: + +``` +1. 安全性:HS256 算法 + 强密钥校验(至少32字节),启动时 fail-close +2. 令牌轮换:刷新时旧 Token 立即加入黑名单,防止重放攻击 +3. 黑名单管理:SHA256 指纹存储,TTL 自动清理,Redis 故障时 fail-open 降级 +4. 防暴力破解:登录失败计数 + 账户锁定(IP+用户名维度) +5. 全局统一:login_required 装饰器 + contextvars 上下文注入,业务代码零侵入 +``` diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/core/login-log.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/core/login-log.md new file mode 100644 index 0000000..0a9fc2f --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/core/login-log.md @@ -0,0 +1,122 @@ +:::tip 登录日志 +登录日志记录每次登录尝试(成功/失败),包含登录账号、IP地址、浏览器信息、登录时间等。支持 IP 地理位置解析(ip2region)和 User-Agent 解析。登录逻辑位于 `src/modules/auth/service.py`,日志记录位于 `src/modules/login_log/service.py`。 +::: + +## 记录时机 + +登录日志在以下时机自动记录: + +``` +1. 登录成功 → 记录成功日志(status=0) +2. 登录失败(密码错误)→ 记录失败日志(status=1) +3. 登录失败(用户不存在)→ 记录失败日志(status=1) +4. 登录失败(账户被锁定)→ 记录失败日志(status=1) +``` + +## 日志字段 + +| 字段 | 说明 | +|------|------| +| `username` | 登录账号 | +| `ip` | 客户端 IP 地址 | +| `ip_location` | IP 地理位置(ip2region 解析) | +| `browser` | 浏览器类型 | +| `os` | 操作系统 | +| `status` | 登录状态:0-成功 1-失败 | +| `message` | 提示信息 | +| `login_time` | 登录时间 | + +## IP 地理位置解析 + +框架集成 ip2region 库,自动将 IP 解析为地理位置: + +```python +# src/utils/ip2region.py +from utils.ip2region import get_ip_location + +location = get_ip_location("116.25.146.177") +# 返回: "广东省深圳市" +``` + +:::tip ip2region +ip2region 是离线 IP 地理位置解析库,数据文件存储在 `static/ip2region.xdb`,无需网络请求,解析速度极快。 +::: + +## User-Agent 解析 + +框架解析请求头中的 User-Agent,提取浏览器和操作系统信息: + +```python +# src/utils/ua.py +from utils.ua import parse_ua + +ua_info = parse_ua(user_agent_string) +# 返回: {"browser": "Chrome 120", "os": "Windows 10"} +``` + +## 查询接口 + +``` +GET /api/v1/login/log/list?pageNo=1&pageSize=10 +GET /api/v1/login/log/detail/{id} +``` + +## 防暴力破解 + +登录日志配合登录失败锁定机制,防止暴力破解。锁定逻辑按 IP+用户名 维度: + +```python +# src/modules/auth/service.py +_LOGIN_FAIL_PREFIX = 'login:fail:' +_LOGIN_LOCK_PREFIX = 'login:lock:' + +def _fail_key(ip: str, username: str) -> str: + """登录失败计数Key:按IP+用户名维度""" + return f'{_LOGIN_FAIL_PREFIX}{ip}:{username}' + +def _lock_key(ip: str, username: str) -> str: + """登录锁定Key""" + return f'{_LOGIN_LOCK_PREFIX}{ip}:{username}' + +async def _is_locked(redis, ip: str, username: str) -> tuple[bool, int]: + """检查是否处于锁定状态,返回 (是否锁定, 剩余秒数)""" + key = _lock_key(ip, username) + ttl = redis.ttl(key) + if ttl and ttl > 0: + return True, ttl + return False, 0 + +async def _record_fail(redis, ip: str, username: str): + """记录一次登录失败,达到上限则锁定""" + key = _fail_key(ip, username) + # 原子 incr + 首次设置过期(Lua 脚本,防止崩溃窗口内 key 永久残留) + count = await incr_with_expire(redis, key, LOGIN_LOCK_SECONDS) + if count >= LOGIN_FAIL_MAX: + lock_k = _lock_key(ip, username) + redis.set(lock_k, '1', ex=LOGIN_LOCK_SECONDS) + +async def _clear_fail(redis, ip: str, username: str): + """登录成功后清除失败记录""" + fail_k = _fail_key(ip, username) + lock_k = _lock_key(ip, username) + redis.delete(fail_k, lock_k) +``` + +配置项: + +| 环境变量 | 默认值 | 说明 | +|---------|--------|------| +| `LOGIN_FAIL_MAX` | `5` | 登录失败次数上限 | +| `LOGIN_LOCK_SECONDS` | `300` | 锁定时间(秒) | + +## 总结 + +登录日志模块具备以下特点: + +``` +1. 自动记录:登录成功/失败自动记录,无需手动埋点 +2. IP 解析:ip2region 离线解析,速度快、无网络依赖 +3. UA 解析:自动提取浏览器和操作系统信息 +4. 防暴力破解:失败计数 + 账户锁定(IP+用户名维度) +5. 安全审计:完整记录登录行为,便于安全审计 +``` diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/core/login-protect.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/core/login-protect.md new file mode 100644 index 0000000..b6b5599 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/core/login-protect.md @@ -0,0 +1,102 @@ +# 登录防爆破 + +:::tip 说明 +登录防爆破机制基于 Redis 实现同一 IP + 用户名维度的连续失败计数与锁定,防止暴力破解密码。配置项通过 `.env` 环境变量控制。 +::: + +## 配置项 + +| 环境变量 | 默认值 | 说明 | +|---------|--------|------| +| `LOGIN_FAIL_MAX` | `5` | 同一 IP+用户名连续失败次数上限 | +| `LOGIN_LOCK_SECONDS` | `600` | 锁定时长(秒) | + +## 工作原理 + +``` +1. 用户提交登录请求 +2. 检查 Redis 中 login:lock:{ip}:{username} 是否存在 + ├─ 存在 → 返回"账号已锁定,请N秒后再试" + └─ 不存在 → 继续验证 +3. 验证用户名密码 + ├─ 成功 → 清除失败计数,生成 Token + └─ 失败 → 累计 login:fail:{ip}:{username} + ├─ 未达上限 → 返回"用户名或密码错误" + └─ 达到上限 → 设置锁定 key(TTL=LOGIN_LOCK_SECONDS),返回"账号已锁定" +``` + +## 核心实现 + +```python +# src/modules/auth/service.py +async def login(self, handler): + ip = get_client_ip(handler.request) + username = handler.get_argument("username", "") + + redis = get_redis() + + # 检查锁定状态 + lock_key = f"login:lock:{ip}:{username}" + is_locked = redis.get(lock_key) + if is_locked: + ttl = redis.ttl(lock_key) + return R.failed(handler, f"登录失败次数过多,请{ttl}秒后再试") + + # 验证用户名密码 + user = self.repo.get_one(username=username) + if not user or not verify_password(...): + # 累计失败次数 + fail_key = f"login:fail:{ip}:{username}" + count = redis.incr(fail_key) + if count == 1: + redis.expire(fail_key, LOGIN_LOCK_SECONDS) + # 达到上限则锁定 + if count >= LOGIN_FAIL_MAX: + redis.set(lock_key, 1, ex=LOGIN_LOCK_SECONDS) + return R.failed(handler, f"登录失败次数过多,请{LOGIN_LOCK_SECONDS}秒后再试") + return R.failed(handler, "用户名或密码错误") + + # 登录成功 → 清除失败计数 + redis.delete(f"login:fail:{ip}:{username}") + + # 生成 Token ... +``` + +## Redis Key 设计 + +| Key | 格式 | TTL | 说明 | +|:----|:-----|:----|:-----| +| 失败计数 | `login:fail:{ip}:{username}` | `LOGIN_LOCK_SECONDS` | 累计失败次数 | +| 锁定状态 | `login:lock:{ip}:{username}` | `LOGIN_LOCK_SECONDS` | 锁定标记 | + +## 维度设计 + +采用 **IP + 用户名** 双维度而非单纯 IP 维度,原因: + +| 维度 | 优点 | 缺点 | +|:----|:-----|:-----| +| 仅 IP | 简单 | 共享 IP(公司 NAT)下一人锁定影响所有人 | +| 仅用户名 | 精准 | 攻击者可从不同 IP 暴破同一账号 | +| **IP + 用户名** | 精准且不影响他人 | Redis key 数量稍多 | + +## 登录成功后的处理 + +登录成功后清除该 IP+用户名 的失败计数,避免累计残留: + +```python +redis.delete(f"login:fail:{ip}:{username}") +``` + +锁定 key 无需手动清除(有 TTL 自动过期)。 + +## 总结 + +登录防爆破方案具备以下特点: + +``` +1. IP+用户名双维度:精准锁定,不影响共享 IP 下的其他用户 +2. 阶梯式防护:累计失败次数,达到阈值自动锁定 +3. 自动解锁:锁定 key 有 TTL,到期自动释放 +4. 登录成功清除:成功登录后清除失败计数 +5. Redis 驱动:高性能,支持分布式部署 +``` diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/core/operation-log.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/core/operation-log.md new file mode 100644 index 0000000..3200aa9 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/core/operation-log.md @@ -0,0 +1,125 @@ +:::tip 操作日志 +操作日志装饰器自动记录写请求(POST/PUT/DELETE/PATCH)的操作日志,无需手动埋点。源码位于 `src/common/middleware/operation_log.py`。 +::: + +## 工作原理 + +``` +1. Handler 方法通过 @operation_log 装饰器标记需要记录的操作 +2. 装饰器在 Handler 方法执行前后记录时间和参数 +3. 方法执行完成后同步记录操作日志到数据库 +4. 记录请求参数、响应结果、耗时、操作类型 +``` + +## 排除路径 + +以下路径不记录操作日志: + +```python +_EXCLUDE_PATHS = ( + '/api/v1/login', + '/api/v1/logout', + '/api/v1/captcha', + '/api/v1/upload', + '/api/v1/loginlog', + '/api/v1/operationlog', + '/api/v1/index/menu', + '/api/v1/index/user', +) +``` + +## 操作类型自动推断 + +中间件根据 URL 路径末段自动推断操作类型,映射表定义在 `_TYPE_MAP` 中: + +```python +_TYPE_MAP = { + 'add': 1, # 新增 + 'update': 2, # 修改 + 'delete': 3, # 删除 + 'batchdelete': 4, # 批量删除 + 'status': 5, # 状态设置 + 'resetpwd': 6, # 重置密码 + 'updatepassword': 7, # 修改密码 + 'import': 8, # 导入 + 'export': 9, # 导出 + 'kickout': 10, # 强退 + 'generate': 11, # 生成代码 + 'clear': 12, # 清空数据 + 'auth': 13, # 授权 + 'refresh': 14, # 缓存 + 'openaccount': 15, # 开账号 +} + +def _infer_type(request) -> int: + """从 URL 路径末段推断操作类型,匹配不到返回 0(其它)""" + path = request.path.rstrip('/') + last_segment = path.split('/')[-1].lower() + # 跳过纯数字路径参数(如 /delete/123 → 取 delete) + if last_segment.isdigit(): + parts = path.split('/') + last_segment = parts[-2].lower() if len(parts) >= 2 else '' + return _TYPE_MAP.get(last_segment, 0) +``` + +| 路径末段 | 操作类型 | 类型ID | +|---------|---------|--------| +| `add` | 新增 | 1 | +| `update` | 修改 | 2 | +| `delete` | 删除 | 3 | +| `batchdelete` | 批量删除 | 4 | +| `status` | 状态设置 | 5 | +| `resetpwd` | 重置密码 | 6 | +| `updatepassword` | 修改密码 | 7 | +| `import` | 导入 | 8 | +| `export` | 导出 | 9 | +| `kickout` | 强退 | 10 | +| `generate` | 生成代码 | 11 | +| `clear` | 清空数据 | 12 | +| `auth` | 授权 | 13 | +| `refresh` | 缓存 | 14 | +| `openaccount` | 开账号 | 15 | +| 其它 | 其它 | 0 | + +## 日志字段 + +| 字段 | 说明 | +|------|------| +| `title` | 操作标题(从路由 summary 获取) | +| `type` | 操作类型(自动推断) | +| `method` | 路由方法名(如 `v1.level.add`) | +| `url` | 请求路径 | +| `param` | 请求参数(截取前2000字符) | +| `result` | 响应结果(截取前2000字符) | +| `consume_time` | 耗时(毫秒) | +| `status` | 状态:0-成功 1-失败 | +| `error` | 错误信息 | +| `create_user` | 操作人姓名(realname) | + +## 查询接口 + +``` +GET /api/v1/oper/log/list?pageNo=1&pageSize=10 +GET /api/v1/oper/log/detail/{id} +``` + +## 性能优化 + +``` +1. 装饰器模式:仅对标记了 @operation_log 的 Handler 记录日志 +2. 长度截断:参数和结果截取前 2000 字符,防止撑爆日志表 +3. 路径排除:登录/验证码等高频路径不记录 +4. IP 归属地:通过 ip2region 离线库解析,无网络开销 +``` + +## 总结 + +操作日志模块具备以下特点: + +``` +1. 装饰器标记:通过 @operation_log 装饰器标记需要记录的 Handler +2. 类型推断:根据 URL 路径自动推断操作类型 +3. 同步落库:Handler 方法执行完成后同步记录到数据库 +4. 性能友好:长度截断 + 路径排除 +5. 完整信息:记录参数、结果、耗时、操作人、IP 归属地等 +``` diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/core/rbac.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/core/rbac.md new file mode 100644 index 0000000..57e1615 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/core/rbac.md @@ -0,0 +1,228 @@ +:::tip RBAC权限 +采用基于角色的访问控制(RBAC)模型,通过用户-角色-菜单三层关系实现细粒度的权限管理。权限控制贯穿前后端:后端通过 `@permission_required` 装饰器校验(`src/common/middleware/access_decorators.py`),前端通过 `v-perm` 指令控制按钮显隐。 +::: + +## 权限模型 + +``` +用户(User) ──N:N── 角色(Role) ──N:N── 菜单(Menu) + │ + type=0: 菜单(页面路由) + type=1: 节点(权限按钮) +``` + +数据库表关系: + +| 关联表 | 说明 | +|--------|------| +| `tornado_user_role` | 用户-角色关联表 | +| `tornado_role_menu` | 角色-菜单关联表 | +| `tornado_menu` | 菜单表(含权限节点) | + +## 权限节点格式 + +权限节点采用 `sys:module:action` 三段式命名: + +``` +sys:user:list 用户列表 +sys:user:add 用户新增 +sys:user:update 用户编辑 +sys:user:delete 用户删除 +sys:user:resetPwd 重置密码 +sys:role:list 角色列表 +sys:role:add 角色新增 +sys:menu:list 菜单列表 +sys:dept:list 部门列表 +sys:level:list 职级列表 +sys:position:list 岗位列表 +``` + +## 菜单模型 + +```python +# src/modules/menu/models.py +# ============================================================ +# 菜单模型 +# ============================================================ +class Menu(BaseModel): + """菜单模型类""" + # ============================================================ + # 表名配置 + # ============================================================ + __tablename__ = DB_PREFIX + "menu" + __table_comment__ = "菜单表" + + # ============================================================ + # 字段定义 + # ============================================================ + # 菜单名称 + name = Column(String(150), nullable=False, index=True, comment="菜单名称") + # 菜单图标 + icon = Column(String(50), nullable=True, comment="菜单图标") + # 菜单图标(备用) + icon2 = Column(String(50), nullable=True, comment="菜单图标") + # 上级ID + parent_id = Column(Integer, default=0, server_default=text('0'), index=True, comment="上级ID") + # 菜单路径 + path = Column(String(255), nullable=True, comment="菜单路径") + # 菜单组件 + component = Column(String(255), nullable=True, comment="菜单组件") + # 权限节点 + permission = Column(String(150), nullable=True, comment="权限节点") + # 打开方式:0-组件 1-内链 2-外链 + target = Column(Integer, default=1, server_default=text('1'), comment="打开方式:0-组件 1-内链 2-外链") + # 菜单类型:0-菜单 1-节点 + type = Column(Integer, default=0, server_default=text('0'), index=True, comment="菜单类型:0-菜单 1-节点") + # 状态选项 + status = Column(Integer, default=0, server_default=text('0'), index=True, comment="菜单状态:0-正常 1-停用") + # 是否可见:0-可见 1-不可见 + hide = Column(Integer, default=1, server_default=text('1'), comment="是否可见:0-可见 1-不可见") + # 排序 + sort = Column(Integer, default=0, server_default=text('0'), comment="排序") + # 备注 + note = Column(String(255), nullable=True, comment="备注") + + # ============================================================ + # 内置方法 + # ============================================================ + def __str__(self): + """返回菜单ID作为字符串表示""" + return "菜单{}".format(self.id) +``` + +菜单类型说明: + +| type | 说明 | 用途 | +|------|------|------| +| `0` | 菜单 | 页面路由,前端生成菜单导航 | +| `1` | 节点 | 权限按钮,控制操作权限 | + +## 后端权限校验 + +### permission_required 装饰器 + +`src/common/middleware/access_decorators.py` 中的 `@permission_required` 装饰器用于端点级权限校验: + +```python +from common.middleware.access_decorators import permission_required, check_demo + +class LevelAddHandler(BaseHandler): + @permission_required("sys:level:add") + @check_demo + async def post(self): + return await level_service.add(self) +``` + +校验逻辑(完整实现): + +```python +def permission_required(permission: str): + """节点权限鉴权装饰器,userId==1(管理员)直接放行""" + def decorator(func): + def _check(self): + """同步权限校验(直查DB,无Redis依赖,供线程池/回退场景使用)""" + userId = current_user_id.get() + if userId == 1: + return None + from menu import service as menu + permission_list = menu.get_permissions_list_sync(userId) + if permission not in permission_list: + return R.failed("权限不足") + return None + + async def _check_cached(self): + """异步权限校验(走 Redis 缓存,未命中才回源DB)""" + userId = current_user_id.get() + if userId == 1: + return None + from menu import service as menu + permission_list = await menu.get_permissions_list(userId) + if permission not in permission_list: + return R.failed("权限不足") + return None + + if asyncio.iscoroutinefunction(func): + @wraps(func) + async def async_wrapper(*args, **kwargs): + request = _extract_request(*args, **kwargs) + denied = await _check_cached(request) + if denied: + return denied + return await func(*args, **kwargs) + return async_wrapper + + @wraps(func) + def sync_wrapper(*args, **kwargs): + request = _extract_request(*args, **kwargs) + denied = _check(request) + if denied: + return denied + return func(*args, **kwargs) + return sync_wrapper + return decorator +``` + +:::warning Request 提取 +装饰器通过 `_extract_request()` 从函数参数中提取 Request 对象,支持 `request` 关键字参数或位置参数两种方式。如果端点函数没有 request 参数,权限校验将被跳过。 +::: + +:::tip 管理员免检 +用户ID为1的管理员账号(admin)自动跳过所有权限校验,拥有全部权限。 +::: + +### 同步/异步适配 + +装饰器自动适配同步和异步端点: + +- **异步端点**:走 Redis 缓存的 `get_permissions_list()`,性能更优 +- **同步端点**:走直接查询 DB 的 `get_permissions_list_sync()`,避免跨事件循环问题 + +## 前端权限控制 + +### v-perm 指令 + +前端使用自定义 `v-perm` 指令控制按钮显隐: + +```html + +新增 + + +操作 +``` + +### 路由守卫 + +前端路由守卫根据用户菜单权限动态生成路由,无权限的页面不会出现在导航菜单中。 + +## 权限数据查询 + +权限列表通过角色-菜单关联查询: + +```python +# 获取用户的权限节点列表(带 Redis 缓存) +async def get_permissions_list(user_id: int) -> list[str]: + # 1. 先查 Redis 缓存 + # 2. 缓存未命中 → 查 DB(user → role → menu where type=1) + # 3. 返回 permission 字段列表 + pass + +# 同步版本(供同步端点使用) +def get_permissions_list_sync(user_id: int) -> list[str]: + # 直接查 DB,不走缓存 + pass +``` + +## 总结 + +的 RBAC 权限方案具备以下特点: + +``` +1. 三层模型:用户-角色-菜单,支持灵活的角色分配 +2. 节点权限:sys:module:action 格式,细粒度控制每个操作 +3. 前后端统一:后端 @permission_required + 前端 v-perm 指令 +4. 管理员免检:userId=1 自动跳过权限校验 +5. 双路校验:异步端点走 Redis 缓存,同步端点直查 DB(避免跨事件循环) +6. 菜单路由:type=0 菜单生成前端路由,type=1 节点控制按钮权限 +7. 懒加载:装饰器内懒加载业务 service,避免 core → 业务模块顶层循环依赖 +``` diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/core/trace-id.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/core/trace-id.md new file mode 100644 index 0000000..cdb78be --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/core/trace-id.md @@ -0,0 +1,117 @@ +# 链路追踪(TraceID) + +:::tip 说明 +链路追踪通过为每个请求分配唯一的 TraceID,将同一请求的所有日志串联起来,方便问题排查和性能分析。TraceID 在请求进入时生成,注入响应头,并通过 `contextvars` 传递到整个请求链路。 +::: + +## 工作原理 + +``` +请求进入 + │ + ├─ 生成 TraceID(UUID4 短格式) + ├─ 注入到 response header: X-Trace-Id + ├─ 存入 contextvar: current_trace_id + │ + ├─ Handler 处理请求 + │ └─ Service → Repository → DB + │ └─ 日志中自动携带 TraceID + │ + └─ 响应返回(携带 X-Trace-Id 头) +``` + +## 实现 + +### 中间件层 + +```python +# src/common/middleware/trace_id.py +import uuid +from core.context import current_trace_id + +def apply_trace_id(handler): + """为请求生成 TraceID 并注入响应头。""" + trace_id = uuid.uuid4().hex[:16] + current_trace_id.set(trace_id) + handler.set_header("X-Trace-Id", trace_id) +``` + +### 上下文变量 + +```python +# src/core/context.py +import contextvars + +current_trace_id: contextvars.ContextVar[str] = contextvars.ContextVar( + "current_trace_id", default="" +) +current_user_id: contextvars.ContextVar[int] = contextvars.ContextVar( + "current_user_id", default=0 +) +current_username: contextvars.ContextVar[str] = contextvars.ContextVar( + "current_username", default="" +) +current_realname: contextvars.ContextVar[str] = contextvars.ContextVar( + "current_realname", default="" +) +``` + +### 在 BaseHandler 中调用 + +```python +# src/core/base_handler.py +class BaseHandler(tornado.web.RequestHandler): + def set_default_headers(self): + super().set_default_headers() + cros_required(self) + apply_trace_id(self) # 注入 TraceID +``` + +## 日志中的 TraceID + +JSON 结构化日志自动携带 TraceID: + +```python +# src/core/logger.py +# 日志格式中包含 trace_id 字段 +{ + "timestamp": "2024-01-15T10:30:00", + "level": "INFO", + "trace_id": "a1b2c3d4e5f6g7h8", + "user_id": 1, + "message": "操作日志记录" +} +``` + +## 使用场景 + +| 场景 | 说明 | +|:-----|:-----| +| 问题排查 | 通过 TraceID 搜索同一请求的所有日志 | +| 性能分析 | 追踪请求在各层的耗时 | +| 异常定位 | 异常日志携带 TraceID,快速定位上下文 | +| 用户反馈 | 用户报告问题时提供 TraceID,精确定位 | + +## 响应头 + +每个 API 响应都携带 `X-Trace-Id` 头: + +```http +HTTP/1.1 200 OK +X-Trace-Id: a1b2c3d4e5f6g7h8 +Content-Type: application/json + +{"code": 0, "data": {...}, "msg": "操作成功", "ok": true} +``` + +## 总结 + +链路追踪方案具备以下特点: + +``` +1. 自动生成:每个请求进入时生成唯一 TraceID(UUID4 短格式) +2. 上下文传递:通过 contextvars 传递到整个请求链路 +3. 响应注入:X-Trace-Id 响应头,前端可记录 +4. 日志关联:JSON 结构化日志自动携带 TraceID +5. 零侵入:业务代码无需感知,自动生效 +``` diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/core/validator.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/core/validator.md new file mode 100644 index 0000000..dc5aef3 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/core/validator.md @@ -0,0 +1,207 @@ +:::tip 参数验证 +使用 Pydantic v2 进行请求参数验证。所有请求体参数通过 Pydantic BaseModel 定义,Tornado 自动解析和验证 JSON 请求体,验证失败返回标准格式错误信息。BaseSchema 基类位于 `src/core/base_schema.py`。 +::: + +## 基本用法 + +### 定义表单类 + +```python +# src/modules/position/schemas.py +# +====================================================================== +# | 模块: 岗位表单验证 +# | 说明: 岗位创建/编辑/状态更新的请求数据校验 +# +====================================================================== + +from typing import Optional + +from pydantic import BaseModel, Field +from core.base_schema import BaseSchema + + +# ============================================================ +# 岗位表单类 +# ============================================================ +class PositionForm(BaseSchema): + """岗位创建/编辑表单""" + name: str = Field(..., min_length=1, max_length=150, description="岗位名称") + status: int = Field(..., ge=1, le=2, description="岗位状态:1-在用 2-停用") + sort: int = Field(..., ge=0, le=99999, description="岗位排序") + + +# ============================================================ +# 设置状态表单类 +# ============================================================ +class PositionStatusForm(BaseModel): + """岗位状态更新表单""" + id: int = Field(..., gt=0, description="岗位ID") + status: int = Field(..., ge=1, le=2, description="岗位状态:1-在用 2-停用") +``` + +### Handler 使用 + +```python +class PositionAddHandler(BaseHandler): + @permission_required("sys:position:add") + @check_demo + async def post(self): + return await position_service.add(self) +``` + +:::tip 自动验证 +Service 层的 `_validate()` 方法自动完成参数验证。验证失败时返回标准格式错误: +```json +{"code": 1, "data": null, "msg": "name: 字段长度不能少于1个字符"} +``` +::: + +## Field 约束 + +Pydantic v2 提供丰富的字段约束: + +| 约束 | 说明 | 示例 | +|------|------|------| +| `min_length` | 字符串最小长度 | `Field(..., min_length=1)` | +| `max_length` | 字符串最大长度 | `Field(..., max_length=150)` | +| `ge` | 大于等于 | `Field(..., ge=0)` | +| `le` | 小于等于 | `Field(..., le=99999)` | +| `gt` | 大于 | `Field(..., gt=0)` | +| `lt` | 小于 | `Field(..., lt=100)` | +| `pattern` | 正则匹配 | `Field(..., pattern=r'^[a-z]+$')` | + +## 自定义验证器 + +使用 `@field_validator` 装饰器添加自定义验证逻辑: + +```python +from pydantic import BaseModel, Field, field_validator + +class UserForm(BaseSchema): + username: str = Field(..., min_length=1, max_length=50) + email: str = Field(..., max_length=100) + + @field_validator('username') + @classmethod + def validate_username(cls, v): + if not v.isalnum(): + raise ValueError('用户名只能包含字母和数字') + return v + + @field_validator('email') + @classmethod + def validate_email(cls, v): + if '@' not in v: + raise ValueError('邮箱格式不正确') + return v +``` + +## BaseSchema 基类 + +所有创建/编辑表单应继承 `BaseSchema`,自动获得 `id` 字段和空字符串处理: + +```python +# src/core/base_schema.py +# +====================================================================== +# | 模块: 表单基类 +# | 说明: 统一处理前端传入的空字符串 id → None 等通用校验 +# +====================================================================== + +"""表单基类:统一处理前端传入的空字符串 id → None 等通用校验。""" + +from typing import Optional + +from pydantic import BaseModel, Field, field_validator + + +# ============================================================ +# 表单基类 +# ============================================================ +class BaseSchema(BaseModel): + """ + 表单基类 + + 所有创建/编辑表单应继承此类,自动获得: + 1. id 字段:Optional[int],前端传空字符串时自动转为 None + """ + + id: Optional[int] = Field(None, description="主键ID") + + @field_validator('id', mode='before') + @classmethod + def empty_str_to_none(cls, v): + """前端 POST 空字符串 '' 时转为 None,避免 Pydantic 整数解析报错""" + if v == '' or v == 'null' or v == 'undefined': + return None + return v +``` + +:::warning 前端空字符串 +前端某些组件(如 el-select)在未选择时可能传空字符串 `""`,BaseSchema 的 `empty_str_to_none` 验证器会自动将其转为 `None`,避免整数字段解析报错。 +::: + +## 唯一性校验 + +唯一性校验通过 Service 层的 `unique_fields` 声明实现: + +```python +class PositionService(BaseService): + model = Position + repo_cls = PositionRepository + unique_fields = {'name': '岗位名称不能重复'} +``` + +`BaseService._check_unique()` 在 `add()` 和 `update()` 时自动查询数据库判断是否重复,无需在 Schema 中手写校验逻辑。 +``` + +## 登录表单示例 + +```python +# src/modules/auth/schemas.py +# +====================================================================== +# | 模块: 登录表单验证 +# | 说明: 用户登录的请求数据校验 +# +====================================================================== + +from pydantic import BaseModel, Field + +from core.config import CAPTCHA_LENGTH + + +# ============================================================ +# 登录表单类 +# ============================================================ +class LoginForm(BaseModel): + """登录表单验证类""" + username: str = Field(..., min_length=1, max_length=20, description="登录账号") + password: str = Field(..., min_length=6, max_length=128, description="登录密码") + code: str = Field(..., min_length=CAPTCHA_LENGTH, max_length=CAPTCHA_LENGTH, description="验证码") + key: str = Field(..., min_length=1, description="KEY值") +``` + +## 错误响应格式 + +验证失败时,全局异常处理器捕获 `RequestValidationError` 并返回标准格式: + +```json +{ + "code": 1, + "data": null, + "msg": "name: 字段长度不能少于1个字符/status: 输入值必须大于等于1", + "ok": false +} +``` + +多个错误用 `/` 分隔。 + +## 总结 + +参数验证模块具备以下特点: + +``` +1. Pydantic v2:类型安全、自动解析 JSON 请求体 +2. 丰富约束:min_length、max_length、ge、le、gt、lt、pattern +3. 自定义验证:@field_validator 实现复杂校验逻辑 +4. BaseSchema 基类:统一 id 字段处理,兼容前端空字符串 +5. 唯一性校验:验证器中查询数据库,确保字段唯一性 +6. 标准错误:全局异常处理器统一错误响应格式 +``` diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/api.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/api.md new file mode 100644 index 0000000..997c350 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/api.md @@ -0,0 +1,332 @@ +# API 层开发 + +:::tip 说明 +前端 API 层位于 `src/api/` 目录,封装所有与后端的 HTTP 请求。底层基于 Axios 封装了 `VAxios` 类(位于 `src/utils/http/axios/`),提供请求拦截、响应解包、错误处理、重复请求取消等能力。 +::: + +## 目录结构 + +``` +src/api/ +├── common/ +│ ├── user.ts # 登录、登出、获取用户信息、验证码 +│ └── menu.ts # 获取菜单(adminMenus) +├── system/ +│ ├── position.ts # 岗位管理接口 +│ ├── user.ts # 用户管理接口 +│ ├── role.ts # 角色管理接口 +│ ├── menu.ts # 菜单管理接口 +│ ├── dept.ts # 部门管理接口 +│ ├── level.ts # 职级管理接口 +│ ├── dictionary.ts # 字典管理接口 +│ ├── loginLog.ts # 登录日志接口 +│ ├── operLog.ts # 操作日志接口 +│ └── tenant.ts # 租户管理接口 +└── ... +``` + +## Axios 封装架构 + +的 HTTP 层不是简单的 Axios 实例,而是一套完整的封装体系: + +``` +src/utils/http/axios/ +├── index.ts # 入口 +├── Axios.ts # Axios 封装类 +├── axiosTransform.ts # 拦截器钩子接口 +├── axiosCancel.ts # 请求取消工具 +├── checkStatus.ts # 状态码错误提示 +├── helper.ts # 工具函数 +└── types.ts # 类型定义 +``` + +### VAxios 类 + +`Axios.ts` 中的 `VAxios` 类是对 `AxiosInstance` 的封装,核心能力: + +```typescript +// src/utils/http/axios/Axios.ts +export class VAxios { + private axiosInstance: AxiosInstance; + private options: CreateAxiosOptions; + + constructor(options: CreateAxiosOptions) { + this.options = options; + this.axiosInstance = axios.create(options); + this.setupInterceptors(); // 自动注册拦截器 + } + + // 通用请求方法 + request(config: AxiosRequestConfig, options?: RequestOptions): Promise { + let conf: AxiosRequestConfig = cloneDeep(config); + const transform = this.getTransform(); + const opt: RequestOptions = Object.assign({}, requestOptions, options); + const { beforeRequestHook, transformRequestData } = transform || {}; + + if (beforeRequestHook) conf = beforeRequestHook(conf, opt); + conf.requestOptions = opt; + + return new Promise((resolve, reject) => { + this.axiosInstance.request>(conf) + .then((res) => { + if (transformRequestData && !axios.isCancel(res)) { + const ret = transformRequestData(res, opt); + resolve(ret); + } + }) + .catch((e) => reject(e)); + }); + } + + // 文件上传 + uploadFile(config: AxiosRequestConfig, params: UploadFileParams) { + const formData = new window.FormData(); + formData.append(params.name || 'file', params.file); + + return this.axiosInstance.request({ + method: 'POST', + data: formData, + headers: { 'Content-type': ContentTypeEnum.FORM_DATA }, + ...config, + }); + } +} +``` + +### 全局实例 + +`index.ts` 导出全局单例 `http`,所有 API 函数使用此实例: + +```typescript +// src/utils/http/axios/index.ts +export const http = createAxios(); +``` + +## API 定义示例 + +以岗位管理为例(`src/api/system/position.ts`),实际代码如下: + +```typescript +import { http } from '@/utils/http/axios'; + +// 分页查询岗位列表 +export function getPositionPage(params?) { + return http.request({ + url: '/position/page', + method: 'GET', + params, + }); +} + +// 获取全部岗位列表(无分页,用于下拉选择) +export function getPositionList(params?) { + return http.request({ + url: '/position/list', + method: 'GET', + params, + }); +} + +// 根据 ID 获取详情 +export function getPositionDetail(positionId) { + return http.request({ + url: '/position/detail/' + positionId, + method: 'get', + }); +} + +// 添加岗位 +export function positionAdd(data: any) { + return http.request({ + url: '/position/add', + method: 'POST', + data, + }); +} + +// 更新岗位 +export function positionUpdate(data: any) { + return http.request({ + url: '/position/update', + method: 'PUT', + data, + }); +} + +// 删除岗位 +export function positionDelete(positionId) { + return http.request({ + url: '/position/delete/' + positionId, + method: 'DELETE', + }); +} + +// 批量删除岗位 +export function positionBatchDelete(data: any) { + return http.request({ + url: '/position/batchDelete', + method: 'DELETE', + data, + }); +} +``` + +:::tip API 路径说明 +API 函数中的 `url` 不需要写 `/api/v1` 前缀。`VAxios` 创建时配置了 `prefixUrl` 和 `requestOptions.urlPrefix`,拦截器的 `beforeRequestHook` 会自动拼接前缀。最终请求路径为 `/api/v1/position/page`。 +::: + +## 请求拦截器 + +请求拦截器在 `index.ts` 的 `transform` 中定义,自动注入 Token: + +```typescript +requestInterceptors: (config, options) => { + const token = storage.get('ACCESS-TOKEN', ''); + if (token && config.requestOptions?.withToken !== false) { + (config as Recordable).headers.Authorization = options.authenticationScheme + ? `${options.authenticationScheme} ${token}` + : token; + } + return config; +}, +``` + +Token 存储在 `localStorage` 中,键名为 `ACCESS-TOKEN`,通过 `storage` 工具类管理(支持 7 天过期)。 + +## 响应拦截器 + +响应解包逻辑在 `transformRequestData` 中处理: + +```typescript +transformRequestData: (res: AxiosResponse, options: RequestOptions) => { + const { code, data, msg } = res.data; + + // 业务成功(code === 0) + if (code === ResultEnum.SUCCESS) { + return data; // 直接返回 data 字段,调用方拿到的是业务数据 + } + + // 业务失败(code === 1) + if (code === ResultEnum.ERROR) { + ElMessage.error(msg); + throw new Error(msg); + } + + // 登录超时(code === 401) + if (code === ResultEnum.TIMEOUT) { + ElMessageBox.confirm('登录身份已失效,请重新登录!', '提示', { ... }) + .then(() => { + storage.clear(); + window.location.href = '/login'; + }); + throw new Error(msg); + } +}, +``` + +解包后的返回值约定: + +| 接口类型 | 返回值结构 | 说明 | +|----------|-----------|------| +| 普通接口 | `data`(直接返回业务数据) | 拦截器已解包 `res.data.data` | +| 分页接口 | `{ data: [...], count: 100 }` | 后端通过 `R.ok(data=list, count=total)` 返回 | +| 登录接口 | `{ access_token: "..." }` | 使用 `isTransformResponse: false`,自行处理 | + +## 响应头 Token 自动续签 + +后端支持滑动窗口续签,响应拦截器会检测 `x-refresh-token` 头并自动更新本地存储: + +```typescript +responseInterceptors: (res: AxiosResponse) => { + const newToken = res.headers['x-refresh-token']; + if (newToken) { + storage.set('ACCESS-TOKEN', newToken); + } + return res; +}, +``` + +## 重复请求取消 + +`AxiosCanceler` 类通过 `pendingMap` 维护请求标识,相同请求自动取消前一个: + +```typescript +// src/utils/http/axios/axiosCancel.ts + +export class AxiosCanceler { + addPending(config: AxiosRequestConfig) { + this.removePending(config); // 先移除已有的同请求 + const url = getPendingUrl(config); + config.cancelToken = new axios.CancelToken((cancel) => { + if (!pendingMap.has(url)) { + pendingMap.set(url, cancel); + } + }); + } + + removePending(config: AxiosRequestConfig) { + const url = getPendingUrl(config); + if (pendingMap.has(url)) { + pendingMap.get(url)(url); // 取消请求 + pendingMap.delete(url); + } + } +} +``` + +## 接口调用方式 + +在 Vue 组件中调用 API: + +```typescript +import { getPositionList, positionAdd } from '@/api/system/position' + +// 查询列表(loadDataTable 传给 BasicTable 的 request 属性) +const loadDataTable = async (res: any) => { + const result = await getPositionList({ ...formParams, ...res }); + return result; // 拦截器已解包,result 即为 { data, count, msg, ok, code } +}; + +// 添加数据 +const handleSubmit = async () => { + await formRef.value?.validate(); + await positionAdd(formData); + message('操作成功'); + emit('success'); +}; +``` + +:::tip 温馨提示 +所有 API 函数返回的是已解包的响应数据(经过拦截器处理),分页接口直接返回 `{ data, count }` 结构,调用方无需再 `.data` 解包。登录接口使用 `isTransformResponse: false` 跳过自动解包,自行处理返回值。 +::: + +## 错误处理 + +错误处理分两层: + +1. **业务错误**(code !== 0):拦截器统一弹出 `ElMessage.error(msg)`,并 `throw new Error` +2. **HTTP 错误**(网络异常、超时等):`responseInterceptorsCatch` 处理 + +```typescript +responseInterceptorsCatch: (error: any) => { + const { response, code } = error || {}; + // 超时 + if (code === 'ECONNABORTED') { + ElMessage.error('接口请求超时,请刷新页面重试!'); + return; + } + // 网络异常 + if (error.toString().includes('Network Error')) { + ElMessageBox.confirm('请检查您的网络连接是否正常', '网络异常', { ... }); + return Promise.reject(error); + } + // 其他 HTTP 状态码 + checkStatus(error.response?.status, response?.data?.msg); +}, +``` + +`checkStatus` 函数根据 HTTP 状态码弹出对应中文提示(400/401/403/404/405/408/500/501/502/503/504/505)。 + +## 总结 + +API 层通过 `VAxios` 类封装 Axios,提供请求拦截(自动注入 Token)、响应解包(`code===0` 返回 `data`)、重复请求取消(`AxiosCanceler`)、Token 自动续签(`x-refresh-token` 响应头)、错误处理(业务错误 + HTTP 状态码)等能力。各模块按目录组织接口函数,调用方拿到的是已解包的业务数据。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/build.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/build.md new file mode 100644 index 0000000..4d43404 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/build.md @@ -0,0 +1,526 @@ +# 前端构建 + +:::tip 说明 +前端使用 Vite 构建,支持开发服务器和生产构建。`vite.config.ts` 通过环境变量和插件系统管理构建配置,生产环境推荐使用 Nginx 部署。 +::: + +## 构建架构 + +``` +vite.config.ts + │ + ├─ loadEnv(mode) ← 按 mode 加载 .env / .env.{mode} + ├─ wrapperEnv() ← 类型转换(string→number/boolean/JSON) + ├─ createVitePlugins() ← 组装插件链 + ├─ createProxy() ← 生成代理配置 + │ + ▼ +┌──────────────────────────────────────────────────────┐ +│ Vite 构建 │ +│ ├─ 开发:vite dev(HMR + proxy) │ +│ └─ 生产:vite build → postBuild → app.config.js │ +└──────────────────────────────────────────────────────┘ +``` + +## 开发环境 + +```bash +# 安装依赖 +pnpm install + +# 启动开发服务器(HMR 热更新) +pnpm dev + +# 等效命令 +pnpm serve +``` + +开发服务器默认运行在 `http://localhost:8001`(端口由 `VITE_PORT` 控制),通过 Vite 代理将 `/api` 请求转发到后端。 + +### 多环境开发命令 + +| 命令 | mode | 加载的 env 文件 | 说明 | +|------|------|----------------|------| +| `pnpm dev` | `development` | `.env` + `.env.development` | 本地开发(默认) | + +--- + +## 环境变量体系 + +### env 文件加载规则 + +Vite 按 mode 加载环境变量,后面的文件覆盖前面的: + +| 文件 | 加载时机 | 说明 | +|------|----------|------| +| `.env` | 所有模式 | 通用配置(端口、标题、短名) | +| `.env.development` | `pnpm dev` | 开发配置(代理、mock) | +| `.env.production` | `pnpm build` | 生产配置 | +| `.env.test` | `pnpm build:test` | 测试环境 | +| `.env.uat` | `pnpm build:uat` | UAT 环境 | +| `.env.dev` | `pnpm build:dev` | 开发联调环境 | + +### 环境变量完整参考 + +| 变量 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `VITE_PORT` | `number` | `8001` | 开发服务器端口 | +| `VITE_PUBLIC_PATH` | `string` | `/` | 部署基础路径(子目录部署时修改) | +| `VITE_GLOB_APP_TITLE` | `string` | `Tornado+ElementPlus版` | 应用标题(注入 HTML) | +| `VITE_GLOB_APP_SHORT_NAME` | `string` | `Tornado+ElementPlus版` | 应用短名(用于生成运行时配置变量名) | +| `VITE_GLOB_API_URL` | `string` | 空 | API 基础路径(留空使用相对路径) | +| `VITE_GLOB_API_URL_PREFIX` | `string` | `/api` | API 前缀 | +| `VITE_GLOB_FILE_URL` | `string` | - | 文件访问域名(拼接上传文件 URL) | +| `VITE_GLOB_UPLOAD_URL` | `string` | - | 图片上传地址 | +| `VITE_GLOB_IMG_URL` | `string` | - | 图片前缀地址 | +| `VITE_GLOB_PROD_MOCK` | `boolean` | `false` | 生产环境是否开启 mock | +| `VITE_USE_MOCK` | `boolean` | `false` | 开发环境是否开启 mock | +| `VITE_DROP_CONSOLE` | `boolean` | `true` | 构建时删除 console | +| `VITE_PROXY` | `JSON` | `[["/api","http://127.0.0.1:8041/api/v1"]]` | 跨域代理配置(JSON 数组) | +| `VITE_BUILD_COMPRESS` | `string` | `'none'` | 压缩方式:`gzip` / `brotli` / `none` | +| `VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE` | `boolean` | `false` | 压缩后是否删除原文件 | + +### 代理配置格式 + +`VITE_PROXY` 是 JSON 格式的二维数组,每个元素 `[前缀, 目标地址]`: + +```bash +# 单个代理 +VITE_PROXY=[["/api","http://127.0.0.1:8041/api/v1"]] + +# 多个代理(注意不要换行) +VITE_PROXY=[["/api","http://127.0.0.1:8041/api/v1"],["/ws","ws://127.0.0.1:8041/ws"]] +``` + +代理生成逻辑(`build/vite/proxy.ts`): + +```typescript +// 输入: [["/api", "http://127.0.0.1:8041/api/v1"]] +// 输出: +{ + '/api': { + target: 'http://127.0.0.1:8041/api/v1', + changeOrigin: true, // 修改 Origin 头 + ws: true, // 支持 WebSocket + rewrite: (path) => path.replace(/^\/api/, ''), // 去掉前缀 + } +} +``` + +### 代码中使用环境变量 + +```typescript +// import.meta.env 包含所有 VITE_ 开头的环境变量 +const apiBaseUrl = import.meta.env.VITE_GLOB_API_URL; +const fileUrl = import.meta.env.VITE_GLOB_FILE_URL; + +// 类型声明在 types/global.d.ts +interface ImportMetaEnv { + readonly VITE_GLOB_API_URL: string; + readonly VITE_GLOB_FILE_URL: string; + // ... +} +``` + +### wrapperEnv 类型转换 + +`build/utils.ts` 中的 `wrapperEnv` 函数自动转换环境变量类型: + +| 原始值 | 转换结果 | +|--------|----------| +| `"true"` | `true`(布尔) | +| `"false"` | `false`(布尔) | +| `"8001"`(VITE_PORT) | `8001`(数字) | +| `'[["/api","http://..."]]'`(VITE_PROXY) | `[["/api","http://..."]]`(JSON 解析) | +| 其他字符串 | 保持原样 | + +--- + +## 插件系统 + +`build/vite/plugin/index.ts` 组装所有 Vite 插件: + +### 插件列表 + +| 插件 | 说明 | 生效时机 | +|------|------|----------| +| `@vitejs/plugin-vue` | Vue3 SFC 支持 | 始终 | +| `@vitejs/plugin-vue-jsx` | JSX/TSX 支持 | 始终 | +| `vite-plugin-vue-setup-extend` | ` +``` + +### 运行时配置(app.config.js) + +构建后自动生成 `dist/app.config.js`,包含 `VITE_GLOB_` 开头的环境变量: + +```javascript +// dist/app.config.js(构建后自动生成,不可手动修改) +window.__PRODUCTION_TORNADO_ELEMENTPLUS___CONF__ = { + "VITE_GLOB_API_URL": "", + "VITE_GLOB_API_URL_PREFIX": "/api", + "VITE_GLOB_FILE_URL": "http://file.tornado.elevue2", + "VITE_GLOB_APP_TITLE": "Tornado+ElementPlus版", + // ... +}; +Object.freeze(window.__PRODUCTION_TORNADO_ELEMENTPLUS___CONF__); +``` + +:::warning 为什么需要运行时配置? +`import.meta.env` 的值在构建时被静态替换,构建后无法修改。而 `app.config.js` 是运行时加载的,部署时只需修改这个文件即可切换 API 地址等配置,无需重新构建。 +::: + +### 打包体积分析 + +```bash +# 生成打包体积报告(自动打开浏览器) +pnpm report +``` + +生成 `node_modules/.cache/visualizer/stats.html`,可视化展示各模块占比。 + +--- + +## 生产构建 + +### 构建命令 + +| 命令 | mode | 说明 | +|------|------|------| +| `pnpm build` | `production` | 生产构建(默认) | +| `pnpm build:prod` | `production` | 等效于 `pnpm build` | +| `pnpm build:test` | `test` | 测试环境构建 | +| `pnpm build:uat` | `uat` | UAT 环境构建 | +| `pnpm build:dev` | `dev` | 开发联调环境构建 | +| `pnpm build:no-cache` | `production` | 清除缓存后构建 | + +### 构建流程 + +``` +pnpm build + │ + ├─ 1. cross-env NODE_ENV=production + │ 设置 Node 环境变量 + │ + ├─ 2. vite build --mode production + │ 加载 .env + .env.production + │ 执行 Vite 构建(代码编译、Tree-shaking、代码分割) + │ 输出到 dist/ + │ + └─ 3. esno ./build/script/postBuild.ts + 运行 postBuild 脚本 + 生成 dist/app.config.js(运行时配置) +``` + +### 构建产物 + +``` +dist/ +├── index.html # 入口 HTML(注入标题和 app.config.js) +├── app.config.js # 运行时配置(VITE_GLOB_* 变量) +├── favicon.ico # 网站图标 +└── assets/ + ├── index-[hash].js # 主包(Vue、ElementPlus、Pinia 等) + ├── index-[hash].css # 全局样式 + ├── [name]-[hash].js # 路由懒加载 chunk(按视图分割) + └── ... # 字体、图片等静态资源 +``` + +### 代码分割策略 + +Vite 自动进行代码分割,主要手段: + +**1. 路由懒加载**(最大收益) + +```typescript +// router 中使用 import.meta.glob 自动分割 +const viewsModules = import.meta.glob('../views/**/*.{vue,tsx}'); + +// 每个视图组件被打包为独立 chunk +// 首屏只加载当前路由的 chunk,其他按需加载 +``` + +**2. 第三方库分割** + +Vite 自动将 `node_modules` 中的库分割为独立 chunk: + +| chunk | 内容 | +|-------|------| +| `vendor-vue.js` | vue + vue-router + pinia | +| `vendor-element.js` | element-plus | +| `vendor-echarts.js` | echarts(如果使用) | +| `vendor-*.js` | 其他大型依赖 | + +**3. 预构建依赖** + +`optimizeDeps.include` 预构建以下依赖(开发时加速冷启动): + +```typescript +optimizeDeps: { + include: ['dayjs', '@vicons/ionicons5', '@vicons/antd', '@element-plus/icons-vue'], +} +``` + +--- + +## 应用初始化 + +`src/main.ts` 是前端入口,初始化顺序: + +```typescript +async function bootstrap() { + const app = createApp(App); + + // 1. 全局注册 ElementPlus 图标 + for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component); + } + + // 2. 全局引入 ElementPlus(中文包) + setupElement(app); + + // 3. 全局注册自定义组件(BasicTable、BasicForm 等) + setupCustomComponents(app); + + // 4. 注册全局指令(v-perm、v-perms、v-scrollBar) + setupDirectives(app); + + // 5. 挂载 Pinia 状态管理 + setupStore(app); + + // 6. 挂载路由 + await setupRouter(app); + + // 7. 路由就绪后挂载应用 + await router.isReady(); + app.mount('#app', true); +} +``` + +### 全局注册清单 + +**插件注册**(`src/plugins/`): + +| 注册项 | 文件 | 说明 | +|--------|------|------| +| ElementPlus | `element.ts` | 全局引入 + 中文包 | +| 自定义组件 | `customComponents.ts` | BasicTable、BasicForm、PageWrapper 等 | +| 自定义指令 | `directives.ts` | `v-perm`、`v-perms`、`v-scrollBar` | + +**自定义指令**: + +| 指令 | 语法 | 说明 | +|------|------|------| +| `v-perm` | `v-perm="['sys:user:add']"` | 包含其中任一权限即显示 | +| `v-perms` | `v-perms="['sys:user:add','sys:user:edit']"` | 包含所有权限才显示 | +| `v-scrollBar` | `v-scrollBar` | 自定义滚动条样式 | + +--- + +## Nginx 部署 + +### 基础配置 + +```nginx +server { + listen 80; + server_name your-domain.com; + charset utf-8; + + # Gzip 压缩 + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml; + gzip_min_length 1000; + gzip_comp_level 6; + + # 前端静态文件 + location / { + root /path/to/dist; + index index.html; + try_files $uri $uri/ /index.html; # SPA 路由回退 + + # 静态资源缓存(带 hash 的文件长期缓存) + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 30d; + add_header Cache-Control "public, immutable"; + } + } + + # API 代理 + location /api/ { + proxy_pass http://127.0.0.1:8041/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 超时配置 + proxy_connect_timeout 60s; + proxy_read_timeout 120s; + proxy_send_timeout 60s; + } + + # 文件访问代理(上传文件) + location /api/file/ { + alias /path/to/uploads/; + expires 7d; + add_header Cache-Control "public"; + } +} +``` + +### 子目录部署 + +如果部署在子目录(如 `https://example.com/admin/`): + +```bash +# .env.production +VITE_PUBLIC_PATH=/admin/ +``` + +```nginx +location /admin/ { + alias /path/to/dist/; + index index.html; + try_files $uri $uri/ /admin/index.html; +} +``` + +--- + +## Docker 部署 + +### 前端独立部署 + +```dockerfile +FROM nginx:alpine + +# 复制构建产物 +COPY dist/ /usr/share/nginx/html/ + +# 复制 Nginx 配置 +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] +``` + +### 前后端一体部署 + +项目根目录的 `docker-compose.yml` 和 `Dockerfile` 支持前后端一体部署,详见 [Docker 部署](/zh/volume6/docker) 章节。 + +--- + +## 构建信息注入 + +`vite.config.ts` 通过 `define` 注入构建信息: + +```typescript +define: { + __APP_INFO__: JSON.stringify({ + pkg: { dependencies, devDependencies, name, version }, + lastBuildTime: formatToDateTime(new Date()), + }), +}, +``` + +代码中访问: + +```typescript +const appInfo = __APP_INFO__; +console.log(appInfo.pkg.version); // package.json 版本 +console.log(appInfo.lastBuildTime); // 最后构建时间 +``` + +--- + +## 常见问题 + +### 开发环境 API 请求 404 + +**原因**:代理配置错误或后端未启动。 + +**排查**: + +1. 检查 `.env.development` 中 `VITE_PROXY` 格式是否正确 +2. 检查后端是否运行在 `http://127.0.0.1:8041` +3. 检查浏览器 Network 面板中请求的实际 URL + +### 构建后页面空白 + +**可能原因**: + +1. `VITE_PUBLIC_PATH` 配置与实际部署路径不一致 +2. Nginx 缺少 `try_files $uri $uri/ /index.html` 导致刷新 404 +3. `app.config.js` 未正确加载(检查 HTML 中的 script 标签) + +**解决**:检查 `dist/index.html` 中的资源路径是否正确。 + +### 构建体积过大 + +**排查**: + +```bash +# 生成打包体积分析报告 +pnpm report +``` + +**优化手段**: + +1. 路由懒加载(已默认启用) +2. 按需引入 ElementPlus 组件(当前为全量引入) +3. 大型依赖(echarts、wangeditor)按需引入 +4. 开启 gzip 压缩(`VITE_BUILD_COMPRESS=gzip`) + +### 环境变量不生效 + +**原因**:Vite 只识别 `VITE_` 开头的环境变量。 + +**排查**: + +1. 变量名必须以 `VITE_` 开头 +2. 修改 `.env` 文件后需要重启开发服务器 +3. 确认加载的 env 文件与当前 mode 匹配 + +### 热更新不工作 + +**可能原因**: + +1. 文件路径大小写不一致(Windows 不敏感,但 Vite 敏感) +2. 修改了 `vite.config.ts`(需要重启) +3. 修改了 `tsconfig.json`(需要重启) + +--- + +## 总结 + +前端使用 Vite 构建,通过 `.env` 文件体系管理多环境配置,`wrapperEnv` 自动类型转换。插件链包括 Vue3 / JSX / 自动导入 / HTML 注入 / Mock / 压缩 / 体积分析。生产构建自动生成 `app.config.js` 运行时配置,支持不重新构建切换 API 地址。代码分割通过路由懒加载和第三方库自动分割实现。部署推荐 Nginx,需配置 `try_files` 回退和 gzip 压缩。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/components.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/components.md new file mode 100644 index 0000000..ebbf444 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/components.md @@ -0,0 +1,839 @@ +# 公共组件库 + +:::tip 说明 +项目提供丰富的公共组件,位于 `ui/src/components/` 目录,基于 ElementPlus 二次封装,覆盖表格、表单、弹窗、上传、富文本编辑等常见场景。组件通过 `src/plugins/` 中的 `setupCustomComponents` 全局注册,任何页面直接使用,无需单独 import。 +::: + +## 组件总览 + +| 组件 | 路径 | 说明 | +|------|------|------| +| BasicTable | `components/Table/` | 表格(列定义、分页、操作列、全屏、密度、斑马纹) | +| TableAction | `components/Table/` | 操作列按钮组(权限控制、下拉折叠) | +| BasicForm | `components/Form/` | 表单(schemas 配置化、动态渲染、折叠展开) | +| BasicModal | `components/Modal/` | 弹窗(拖拽、全屏、自定义按钮) | +| BasicUpload | `components/Upload/` | 文件/图片上传(卡片/列表、限制数量、预览) | +| Editor | `components/Editor/` | 富文本编辑器(基于 wangeditor) | +| CropperImage | `components/Cropper/` | 图片裁剪(圆形/矩形、实时预览) | +| QrCode | `components/Qrcode/` | 二维码生成(canvas/img、内嵌 logo) | +| ChinaArea | `components/ChinaArea/` | 省市区级联(懒加载、二/三级联动) | +| Icon | `components/icon/` | 图标(支持 ElementPlus 图标 + SVG 图标) | +| BasicSelect | `components/Select/` | 增强下拉(远程请求、本地缓存) | +| CountTo | `components/CountTo/` | 数字动画(前缀/后缀、千分位、缓动曲线) | +| Password | `components/Password/` | 密码强度(强度指示器、重复确认、复杂度校验) | +| PageWrapper | `components/Page/` | 页面容器(标题卡片、固定底部) | +| Authority | `components/Authority/` | 权限容器(按权限码控制子元素渲染) | +| LockScreen | `components/Lockscreen/` | 锁屏组件 | +| ImpExcel | `components/Excel/` | Excel 导入/导出 | +| AppProvider | `components/Application/` | 应用提供者(全局配置注入) | +| Websocket | `components/Websocket/` | WebSocket 连接 | +| importFile | `components/importFile/` | 通用文件导入 | +| numberInput | `components/numberInput/` | 数字输入框 | +| priceInput | `components/priceInput/` | 金额输入框 | +| Region | `components/Region/` | 区域选择 | +| TableSelect | `components/TableSelect/` | 表格选择器 | +| pagination | `components/pagination/` | 分页组件 | + +--- + +## BasicTable 表格 + +表格组件是使用频率最高的组件,基于 `el-table` + `el-pagination` 封装,详见 [BasicTable 表格](/zh/volume4/frontend/table) 章节。 + +### Props + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `columns` | `BasicColumn[]` | `[]`(必填) | 列定义 | +| `request` | `Function` | `null` | 数据请求函数,自动管理分页和 loading | +| `dataSource` | `Function \| Array` | `[]` | 数据源(与 request 二选一) | +| `actionColumn` | `BasicColumn` | `null` | 操作列配置 | +| `title` | `string` | `null` | 表格标题 | +| `titleTooltip` | `string` | `null` | 标题提示 | +| `pagination` | `Object \| Boolean` | `{}` | 分页配置,`false` 隐藏分页 | +| `rowKey` | `string \| Function` | `undefined` | 行数据的唯一标识 | +| `border` | `Boolean` | `false` | 是否显示边框 | +| `striped` | `Boolean` | `false` | 斑马纹 | +| `size` | `string` | `'default'` | 表格尺寸:`small` / `default` / `large` | +| `tableHeight` | `Number \| String` | `null` | 固定表格高度 | +| `maxHeight` | `Number` | - | 最大高度 | +| `canResize` | `Boolean` | `true` | 是否自适应窗口高度 | +| `resizeHeightOffset` | `Number` | `0` | 高度偏移量 | +| `showTableSetting` | `Boolean` | `true` | 是否显示工具栏 | +| `tableSetting` | `TableSetting` | 见下方 | 工具栏配置 | +| `isKeepRowKeys` | `Boolean` | `false` | 翻页时是否保留选中行 | + +### tableSetting 工具栏配置 + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `redo` | `Boolean` | `true` | 刷新按钮 | +| `size` | `Boolean` | `true` | 密度切换 | +| `setting` | `Boolean` | `true` | 列设置 | +| `fullscreen` | `Boolean` | `true` | 全屏切换 | +| `striped` | `Boolean` | `true` | 斑马纹开关 | + +### BasicColumn 列定义 + +| 属性 | 类型 | 说明 | +|------|------|------| +| `label` | `string` | 列标题 | +| `prop` | `string` | 字段名 | +| `width` | `number` | 列宽 | +| `align` | `string` | 对齐方式 | +| `type` | `string` | 类型:`index` / `selection` | +| `render` | `Function` | 自定义渲染函数 `(column, row, index) => VNode` | +| `isSlot` | `boolean` | 是否使用插槽 | +| `ellipsis` | `boolean` | 超出省略(tooltip) | +| `auth` | `string[]` | 权限编码,控制列是否显示 | +| `ifShow` | `boolean \| Function` | 业务控制是否显示 | + +### Events + +| 事件 | 参数 | 说明 | +|------|------|------| +| `fetch-success` | `(data, result)` | 数据请求成功 | +| `fetch-error` | `(error)` | 数据请求失败 | +| `selection-change` | `(rows)` | 多选变化 | +| `columns-change` | `(columns)` | 列设置变化 | +| `checked-row-change` | `(rowKeys)` | 选中行变化 | + +### Slots + +| 插槽 | 说明 | +|------|------| +| `tableTitle` | 表格标题左侧区域(常放添加按钮) | +| `toolbar` | 工具栏右侧区域 | +| `{prop}` | 列名对应插槽,自定义列渲染 | + +### Expose 方法 + +| 方法 | 说明 | +|------|------| +| `reload(opt?)` | 刷新表格(保留分页) | +| `restReload(opt?)` | 重置到第一页并刷新 | +| `reloadTable(opt?)` | 内部刷新(清空选中行) | +| `getDataSource()` | 获取当前数据 | +| `setTableData(data)` | 设置表格数据 | +| `updateTableDataRecord(index, record)` | 更新指定行 | +| `deleteTableDataRecord(index)` | 删除指定行 | +| `getColumns()` | 获取列配置 | +| `setColumns(columns)` | 设置列配置 | +| `clearSelection()` | 清空多选 | +| `setCheckedRowKeys(keys)` | 设置选中行 | +| `redoHeight()` | 重新计算高度 | + +### 基础用法 + +```vue + + + +``` + +--- + +## TableAction 操作列 + +操作列按钮组件,支持权限控制和下拉折叠。 + +### Props + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `actions` | `ActionItem[]` | (必填) | 操作按钮列表 | +| `dropDownActions` | `ActionItem[]` | `null` | 下拉折叠操作列表 | +| `dropDownProps` | `ActionItem` | `{ label: '更多' }` | 下拉按钮配置 | +| `select` | `Function` | `() => {}` | 下拉选择回调 | + +### ActionItem 配置 + +| 属性 | 类型 | 说明 | +|------|------|------| +| `label` | `string` | 按钮文字 | +| `auth` | `string[]` | 权限编码 | +| `onClick` | `Function` | 点击回调 | +| `icon` | `Component` | 图标组件 | +| `type` | `string` | 按钮类型 | +| `disabled` | `boolean` | 是否禁用 | +| `ifShow` | `boolean \| Function` | 是否显示 | +| `isConfirm` | `boolean` | 是否需要确认 | +| `popConfirm` | `object` | 气泡确认框配置 | + +--- + +## BasicForm 表单 + +表单组件通过 schemas 配置化定义表单字段,支持动态渲染、折叠展开,详见 [BasicForm 表单](/zh/volume4/frontend/form) 章节。 + +### Props + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `schemas` | `FormSchema[]` | `[]` | 表单字段配置 | +| `model` | `Object` | - | 表单数据对象 | +| `rules` | `FormRules` | - | 表单验证规则 | +| `inline` | `Boolean` | `false` | 行内表单 | +| `layout` | `string` | `'vertical'` | 布局方式 | +| `labelPosition` | `string` | `'right'` | 标签位置 | +| `labelWidth` | `Number \| String` | `80` | 标签宽度 | +| `size` | `string` | `'default'` | 表单尺寸 | +| `disabled` | `Boolean` | `false` | 禁用全部组件 | +| `isFull` | `Boolean` | `true` | 组件是否 100% 宽度 | +| `showActionButtonGroup` | `Boolean` | `true` | 显示操作按钮组 | +| `showSubmitButton` | `Boolean` | `true` | 显示查询按钮 | +| `showResetButton` | `Boolean` | `true` | 显示重置按钮 | +| `showAdvancedButton` | `Boolean` | `true` | 显示展开/收起按钮 | +| `submitButtonText` | `string` | `'查询'` | 查询按钮文字 | +| `resetButtonText` | `string` | `'重置'` | 重置按钮文字 | +| `collapsed` | `Boolean` | `true` | 是否默认折叠 | +| `collapsedRows` | `Number` | `1` | 折叠时显示行数 | + +### FormSchema 字段配置 + +| 属性 | 类型 | 说明 | +|------|------|------| +| `field` | `string` | 字段名(必填) | +| `label` | `string` | 标签文字 | +| `component` | `ComponentType` | 渲染组件:`Input` / `Select` / `DatePicker` 等 | +| `componentProps` | `object` | 组件属性 | +| `defaultValue` | `any` | 默认值 | +| `rules` | `object \| object[]` | 字段校验规则 | +| `slot` | `string` | 自定义插槽名 | +| `hidden` | `boolean \| Function` | 是否隐藏 | +| `colProps` | `ColProps` | 栅格配置 | +| `labelMessage` | `string` | 标签提示信息 | +| `suffix` | `string` | 后缀文字 | + +### Events + +| 事件 | 说明 | +|------|------| +| `submit` | 点击查询按钮 | +| `reset` | 点击重置按钮 | + +### Expose 方法 + +| 方法 | 说明 | +|------|------| +| `setFieldsValue(values)` | 设置字段值 | +| `getFieldsValue()` | 获取所有字段值 | +| `resetFields()` | 重置字段 | +| `validate()` | 校验表单 | +| `clearValidate(name?)` | 清除校验 | +| `setProps(formProps)` | 设置属性 | +| `setSchema(schemaProps)` | 动态修改 schema | + +### 基础用法 + +```vue + + + +``` + +--- + +## BasicModal 弹窗 + +弹窗组件基于 `el-dialog` 封装,支持拖拽、自定义按钮,详见 [Modal 弹窗](/zh/volume4/frontend/modal) 章节。 + +### Props + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `title` | `string` | `''` | 弹窗标题 | +| `width` | `Number` | `446` | 弹窗宽度 | +| `draggable` | `Boolean` | `true` | 是否可拖拽 | +| `hideFooter` | `Boolean` | `false` | 隐藏底部按钮 | +| `confirmButText` | `string` | `'确认'` | 确认按钮文字 | +| `cancelButText` | `string` | `'取消'` | 取消按钮文字 | +| `confirmButProps` | `Object` | - | 确认按钮属性 | +| `cancelButProps` | `Object` | - | 取消按钮属性 | + +### Events + +| 事件 | 说明 | +|------|------| +| `ok` | 点击确认 | +| `register` | 弹窗注册回调 | + +### Expose 方法 + +| 方法 | 说明 | +|------|------| +| `setModalProps(props)` | 设置弹窗属性 | +| `openModal()` | 打开弹窗 | +| `closeModal()` | 关闭弹窗 | + +### 基础用法 + +```vue + + + +``` + +--- + +## BasicUpload 上传 + +文件/图片上传组件,基于 `el-upload` 封装,支持卡片/列表模式、数量限制、图片预览。 + +### Props + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `listType` | `string` | `'picture-card'` | 展示类型:`picture-card` / `text` / `picture` | +| `limit` | `Number` | `Infinity` | 最大上传数量 | +| `maxSize` | `Number` | `2` | 文件大小上限(MB) | +| `accept` | `string` | - | 接受的文件类型 | +| `list` | `Ifiles[]` | `[]` | 已上传文件列表 | +| `circle` | `Boolean` | `false` | 圆形裁剪 | +| `uploadTitle` | `string` | `'上传图片'` | 列表模式按钮文字 | +| `helpText` | `string` | `''` | 帮助提示文字 | +| `width` | `Number` | `104` | 卡片宽度(px) | + +### Events + +| 事件 | 参数 | 说明 | +|------|------|------| +| `uploadChange` | `(fileList)` | 上传/删除变化 | +| `delete` | `(fileList)` | 删除文件 | + +### 基础用法 + +```vue + + + + + + + + +``` + +--- + +## Editor 富文本编辑器 + +基于 [wangeditor](https://www.wangeditor.cn/) 封装,支持图片/视频上传、工具栏配置。 + +### Props + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `getHtml` | `string` | - | 双向绑定(HTML 内容) | +| `getText` | `string` | - | 双向绑定(纯文本) | +| `height` | `string` | `'310'` | 编辑器高度 | +| `width` | `string` | `'auto'` | 编辑器宽度 | +| `mode` | `string` | `'default'` | 模式:`default` / `simple` | +| `disable` | `Boolean` | `false` | 是否禁用 | +| `placeholder` | `string` | `'请输入内容...'` | 占位文字 | +| `uploadFileUrl` | `string` | `'/file/upload'` | 图片上传接口 | + +### 基础用法 + +```vue + +``` + +:::warning 注意 +编辑器内部自动处理图片上传,上传接口由 `uploadFileUrl` 配置。后端图片迁移和 XSS 清洗由 Service 层的 `rich_text_fields` 声明自动处理。 +::: + +--- + +## CropperImage 图片裁剪 + +基于 [cropperjs](https://github.com/fengyuanchen/cropperjs) 封装,支持圆形/矩形裁剪、实时预览。 + +### Props + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `src` | `string` | (必填) | 图片地址 | +| `alt` | `string` | - | 图片描述 | +| `circled` | `Boolean` | `false` | 圆形裁剪 | +| `realTimePreview` | `Boolean` | `true` | 实时预览 | +| `height` | `string \| Number` | `'360px'` | 裁剪区域高度 | +| `imageStyle` | `CSSProperties` | `{}` | 图片样式 | +| `options` | `Object` | `{}` | cropperjs 配置 | + +### Events + +| 事件 | 参数 | 说明 | +|------|------|------| +| `cropend` | `{ imgBase64, imgInfo }` | 裁剪完成 | +| `ready` | `(cropper)` | 裁剪器就绪 | +| `cropendError` | - | 裁剪出错 | + +### 基础用法 + +```vue + + + +``` + +--- + +## QrCode 二维码生成 + +基于 [qrcode](https://github.com/soldair/node-qrcode) 封装,支持 canvas/img 渲染、内嵌 logo。 + +### Props + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `value` | `string \| Array` | (必填) | 二维码内容 | +| `width` | `Number` | `200` | 宽度(px) | +| `tag` | `string` | `'canvas'` | 渲染方式:`canvas` / `img` | +| `logo` | `string \| Object` | `''` | 中间 logo | +| `options` | `Object` | `{}` | qrcode 配置项 | + +### Events + +| 事件 | 参数 | 说明 | +|------|------|------| +| `done` | `{ url, ctx }` | 生成完成 | +| `error` | `(error)` | 生成失败 | + +### Expose 方法 + +| 方法 | 说明 | +|------|------| +| `download(fileName?)` | 下载二维码图片 | + +### 基础用法 + +```vue + +``` + +--- + +## ChinaArea 省市区级联 + +省市区三级联动选择器,基于后端城市接口懒加载。 + +### Props + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `modelValue` | `String \| Array` | - | 双向绑定(区划编码数组) | +| `type` | `Number` | `3` | 联动级别:`2`(省市) / `3`(省市区) | +| `disabled` | `Boolean` | `false` | 是否禁用 | + +### Events + +| 事件 | 参数 | 说明 | +|------|------|------| +| `update:modelValue` | `(codes)` | 值变化 | +| `change` | `(text)` | 选中文本(如"浙江省杭州市西湖区") | + +### 基础用法 + +```vue + +``` + +--- + +## BasicSelect 增强下拉 + +远程数据下拉选择器,支持请求函数和本地缓存。 + +### Props + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `request` | `Function` | (必填) | 获取选项数据的异步函数 | +| `options` | `Array` | `[]` | 静态选项(与 request 二选一) | +| `modelValue` | `String \| Number` | - | 双向绑定 | +| `cache` | `Boolean` | `false` | 是否缓存到 localStorage | +| `cacheKey` | `string` | `''` | 缓存 key(开启缓存时必填) | +| `width` | `Number` | `150` | 宽度(px) | +| `block` | `Boolean` | `false` | 100% 父容器宽度 | + +### Expose 方法 + +| 方法 | 说明 | +|------|------| +| `fetch()` | 重新请求数据 | +| `getData()` | 获取当前选项数据 | + +### 基础用法 + +```vue + + + +``` + +--- + +## CountTo 数字动画 + +数字动态递增/递减动画,支持前缀/后缀、千分位、缓动曲线。 + +### Props + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `startVal` | `Number` | `0` | 起始值 | +| `endVal` | `Number` | `2021` | 结束值 | +| `duration` | `Number` | `1500` | 动画时长(ms) | +| `autoplay` | `Boolean` | `true` | 自动播放 | +| `decimals` | `Number` | `0` | 小数位数 | +| `prefix` | `string` | `''` | 前缀 | +| `suffix` | `string` | `''` | 后缀 | +| `separator` | `string` | `','` | 千分位分隔符 | +| `decimal` | `string` | `'.'` | 小数点符号 | +| `color` | `string` | - | 字体颜色 | +| `useEasing` | `Boolean` | `true` | 是否使用缓动 | +| `transition` | `string` | `'linear'` | 缓动曲线 | + +### Events + +| 事件 | 说明 | +|------|------| +| `onStarted` | 动画开始 | +| `onFinished` | 动画结束 | + +### 基础用法 + +```vue + +``` + +--- + +## Password 密码强度 + +密码输入框,内置强度指示器、重复确认、复杂度校验。 + +### Props + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `value` | `string` | - | 密码值 | +| `repeat` | `Boolean` | `false` | 是否显示重复确认框 | +| `required` | `Boolean` | `true` | 是否必填 | +| `minLength` | `Number` | `6` | 最小长度 | +| `maxLength` | `Number` | `32` | 最大长度 | +| `complexity` | `Boolean` | `false` | 是否启用复杂度校验 | +| `complexityTip` | `string` | - | 复杂度不满足时的提示 | +| `width` | `Number` | `300` | 弹出框宽度 | +| `block` | `Boolean` | `false` | 100% 宽度 | + +### 强度等级 + +| 等级 | 条件 | 说明 | +|------|------|------| +| 0 | 无 | 极弱 | +| 1 | 含小写或大写 | 弱 | +| 2 | 含小写 + 大写,或含数字 | 中 | +| 3 | 含小写 + 大写 + 数字 | 强 | +| 4 | 含小写 + 大写 + 数字 + 特殊字符 | 极强 | + +### Expose 方法 + +| 方法 | 说明 | +|------|------| +| `isValidator()` | 是否通过校验 | +| `showValidator()` | 触发校验显示 | + +### 基础用法 + +```vue + +``` + +--- + +## PageWrapper 页面容器 + +页面容器组件,提供标题卡片和固定底部区域。 + +### Props + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `title` | `string` | `''` | 页面标题 | +| `content` | `string` | `''` | 标题下方描述文字 | +| `contentStyle` | `Object` | `{}` | 主体区域样式 | +| `contentClass` | `string` | `''` | 主体区域 class | +| `showFooter` | `Boolean` | `true` | 是否显示底部 | + +### Slots + +| 插槽 | 说明 | +|------|------| +| `headerContent` | 标题卡片内容区域 | +| `default` | 页面主体内容 | +| `leftFooter` | 底部左侧(常放操作按钮) | +| `rightFooter` | 底部右侧 | + +### 基础用法 + +```vue + +``` + +--- + +## Authority 权限容器 + +按权限码控制子元素是否渲染,类似 `v-perm` 指令的组件形式。 + +### Props + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `value` | `string[]` | `[]` | 权限编码数组 | + +### 基础用法 + +```vue + +``` + +:::tip v-perm 指令 +除了 `Authority` 组件,项目还提供 `v-perm` 指令,功能相同但更简洁: +```vue +新增用户 +``` +::: + +--- + +## Icon 图标 + +统一图标组件,支持 ElementPlus 图标和 SVG 图标。 + +### Props + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `name` | `string` | (必填) | 图标名称 | +| `size` | `string \| Number` | `'14px'` | 图标大小 | +| `color` | `string` | `'inherit'` | 图标颜色 | + +### 基础用法 + +```vue + +``` + +--- + +## ImpExcel Excel 导入/导出 + +Excel 文件导入和导出组件。 + +### 导出函数 + +```typescript +import { jsonToSheetXlsx, aoaToSheetXlsx } from '@/components/Excel'; + +// JSON 数据导出 +jsonToSheetXlsx({ + data: [{ name: '张三', age: 25 }], + header: { name: '姓名', age: '年龄' }, + filename: '用户列表.xlsx', +}); + +// 二维数组导出 +aoaToSheetXlsx({ + data: [['姓名', '年龄'], ['张三', 25]], + filename: '用户列表.xlsx', +}); +``` + +### 导入组件 + +```vue + + + +``` + +--- + +## 全局注册 + +所有组件在 `src/plugins/setupCustomComponents.ts` 中通过 `setupCustomComponents` 全局注册: + +```typescript +// main.ts +import { setupCustomComponents } from '/@/plugins'; + +setupCustomComponents(app); // 注册所有自定义组件 +``` + +全局注册后,任何组件中直接使用,无需单独 import。 + +--- + +## 权限控制体系 + +项目提供三种权限控制方式,覆盖不同场景: + +| 方式 | 语法 | 适用场景 | +|------|------|----------| +| `v-perm` 指令 | `v-perm="['sys:user:add']"` | 按钮级权限,最常用 | +| `Authority` 组件 | `` | 容器级权限,控制多个元素 | +| `auth` 列属性 | `{ auth: ['sys:user:delete'] }` | TableAction 操作列权限 | + +:::warning 注意 +`v-perm` 和 `Authority` 的权限码必须与后端 `@permission_required("sys:xxx:xxx")` 一致。用户 ID 1(admin)拥有所有权限。 +::: + +--- + +## 总结 + +公共组件库基于 ElementPlus 二次封装,核心三件套 BasicTable(表格)、BasicForm(表单)、BasicModal(弹窗)覆盖列表页标准场景。辅以 Upload、Editor、Cropper 等业务组件,以及 Authority、v-perm 权限体系。通过全局注册,任何页面直接使用,无需单独 import。优先使用公共组件,避免重复开发。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/form.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/form.md new file mode 100644 index 0000000..3b7e505 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/form.md @@ -0,0 +1,240 @@ +# BasicForm 表单 + +:::tip 说明 +`BasicForm` 位于 `src/components/Form/src/BasicForm.vue`,基于 ElementPlus 的 `el-form` 二次封装,通过 schemas 配置化定义表单字段,支持查询表单和业务表单两种模式。 +::: + +## 组件架构 + +``` +src/components/Form/ +├── src/ +│ ├── BasicForm.vue # 主组件 +│ ├── props.ts # 属性定义 +│ ├── helper.ts # 工具函数(placeholder 生成) +│ ├── types/form.ts # 类型定义(FormSchema、FormProps) +│ └── hooks/ +│ ├── useFormEvents.ts # 表单事件(submit、reset、validate) +│ └── useFormValues.ts # 表单值处理(默认值初始化) +└── index.ts # 导出 useForm、FormSchema 等 +``` + +## 查询表单(列表页搜索) + +列表页的搜索区域使用 `useForm` 注册表单: + +```vue + + + +``` + +### querySchemas 定义 + +```typescript +// src/views/system/position/querySchemas.ts + +import { FormSchema } from '@/components/Form/index'; + +export const schemas: FormSchema[] = [ + { + field: 'name', + component: 'Input', + label: '岗位名称', + componentProps: { + placeholder: '请输入岗位名称', + }, + }, + { + field: 'status', + component: 'Select', + label: '状态', + componentProps: { + placeholder: '请选择状态', + clearable: true, + options: [ + { label: '正常', value: '1' }, + { label: '禁用', value: '2' }, + ], + }, + }, +]; +``` + +### useForm 注册 + +```typescript +import { schemas } from './querySchemas'; +import { useForm } from '@/components/Form/index'; +import { ColProps } from 'element-plus'; + +const formParams = reactive({ name: '', status: '' }); + +const [register, {}] = useForm({ + labelWidth: 80, + layout: 'horizontal', + colProps: { span: 6 } as ColProps, + submitOnReset: true, + schemas, +}); + +function handleSubmit(values: Recordable) { + handleReset(); + for (const key in values) { formParams[key] = values[key]; } + reloadTable(); +} + +function handleReset() { + for (const key in formParams) { formParams[key] = ''; } +} +``` + +`useForm` 返回 `[register, formActionType]`,`register` 传给 `@register` 事件完成表单绑定。 + +## 业务表单(编辑弹窗) + +编辑弹窗中直接使用 `el-form` + `el-form-item` 构建表单: + +```vue + + +``` + +## BasicForm 内部渲染机制 + +`BasicForm.vue` 根据 `schema.component` 动态渲染对应的 ElementPlus 组件: + +```vue + + +``` + +## FormSchema 类型 + +```typescript +interface FormSchema { + field: string; // 字段名 + label: string; // 标签文本 + component: string; // 组件类型:Input / Select / RadioGroup / Checkbox / DatePicker 等 + componentProps?: any; // 组件属性(placeholder、options、clearable 等) + required?: boolean; // 是否必填 + rules?: any[]; // 自定义校验规则 + defaultValue?: any; // 默认值 + hidden?: boolean | (({ schema, values, model }) => boolean); // 是否隐藏 + slot?: string; // 自定义插槽名 + suffix?: string; // 后缀插槽名 + labelMessage?: string; // 标签提示信息 + isFull?: boolean; // 是否全宽 + type?: string; // 组件子类型 +} +``` + +## 表单校验 + +```typescript +// 手动校验 +const handleSubmit = async () => { + await formRef.value?.validate(); + // 校验通过,提交数据 +}; + +// 清除校验 +formRef.value?.clearValidate(); +``` + +Schema 中的 `required: true` 会自动生成必填校验规则,也可以通过 `rules` 自定义: + +```typescript +{ + field: 'name', + label: '岗位名称', + component: 'Input', + rules: [ + { required: true, message: '请输入岗位名称', trigger: 'blur' }, + { min: 1, max: 150, message: '长度在 1 到 150 个字符', trigger: 'blur' }, + ], +} +``` + +## 表单赋值 + +编辑时需要将行数据赋值给表单: + +```typescript +const formData = reactive({ id: '', name: '', status: 1, sort: 0 }); + +const setFormData = async () => { + const data = await getPositionDetail(props.positionId); + for (const key in formData) { + if (data[key] != null && data[key] != undefined) { + formData[key] = data[key]; + } + } +}; + +onMounted(() => { + if (props.positionId) setFormData(); +}); +``` + +:::tip 温馨提示 +查询表单的字段名需要与后端 `page_like_fields` 和 `page_eq_fields` 对应,确保查询参数能正确传递到后端。`useForm` 返回的 `register` 函数传给 `@register` 事件完成表单绑定,`formActionType` 提供 `setFieldsValue`、`resetFields`、`validate` 等方法。 +::: + +## 总结 + +BasicForm 通过 schemas 配置化定义表单字段,内部根据 `component` 动态渲染对应的 ElementPlus 组件。查询表单使用 `useForm` 注册并绑定 `@submit`/`@reset` 事件,业务表单直接使用 `el-form` + `reactive` 管理数据。支持多种组件类型、表单校验、数据绑定、条件隐藏等能力。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/hooks.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/hooks.md new file mode 100644 index 0000000..b4f3541 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/hooks.md @@ -0,0 +1,260 @@ +# Hooks 组合式函数 + +:::tip 说明 +项目封装了一系列组合式函数(Hooks),位于 `src/hooks/` 和 `src/utils/` 目录,提供权限检查、异步 loading、防重复提交、实时时钟、项目配置等可复用逻辑。 +::: + +## Hooks 目录结构 + +``` +src/hooks/ +├── event/ +│ ├── useEventListener.ts # DOM 事件监听 +│ ├── useBreakpoint.ts # 响应式断点 +│ └── useWindowSizeFn.ts # 窗口大小变化回调 +├── setting/ +│ ├── useProjectSetting.ts # 项目配置(权限模式、水印等) +│ └── useDesignSetting.ts # 设计配置(主题色、深色模式) +├── web/ +│ ├── usePermission.ts # 权限检查 +│ ├── useECharts.ts # ECharts 封装 +│ ├── usePage.ts # 页面跳转 +│ ├── useTags.ts # 标签页管理 +│ └── useCopyToClipboard.ts # 剪贴板复制 +├── core/ +│ └── useTimeout.ts # 定时器封装 +├── useDomWidth.ts # DOM 宽度 +├── useOnline.ts # 网络状态 +├── useBattery.ts # 电量信息 +├── useTime.ts # 实时时钟 +└── use-async.ts # 异步 loading 管理 +``` + +## usePermission + +权限检查 Hook,从 user store 获取权限列表进行判断: + +```typescript +// src/hooks/web/usePermission.ts + +import { useUserStore } from '@/store/modules/user'; + +export function usePermission() { + const userStore = useUserStore(); + + function _somePermissions(accesses: string[]) { + const permissionsList = userStore.getPermissions; + if (permissionsList[0] == '*:*:*') return true; // 超级管理员 + return accesses.some((item) => permissionsList.includes(item)); + } + + // 是否包含其中某个权限(用于 v-perm 指令) + function hasSomePermission(accesses?: string | string[]): boolean { + const permissionsList = userStore.getPermissions; + if (permissionsList[0] === '*:*:*') return true; + const list = Array.isArray(accesses) ? accesses : []; + return list.some((access) => permissionsList.includes(access)); + } + + // 是否包含所有权限(用于 v-perms 指令) + function hasEveryPermission(accesses?: string | string[]): boolean { + const list = Array.isArray(accesses) ? accesses : []; + const permissionsList = userStore.getPermissions; + if (permissionsList[0] === '*:*:*') return true; + return list.every((access) => permissionsList.includes(access)); + } + + // 判断是否存在权限(可用于 v-if 显示逻辑) + function hasPermission(accesses: string[]): boolean { + if (!accesses || !accesses.length) return true; + return _somePermissions(accesses); + } + + return { hasPermission, hasEveryPermission, hasSomePermission }; +} +``` + +使用示例: + +```typescript +import { usePermission } from '@/hooks/web/usePermission'; + +const { hasPermission, hasSomePermission } = usePermission(); + +// v-if 条件渲染 +const showAddBtn = computed(() => hasPermission(['sys:position:add'])); + +// 检查是否有任一权限 +const canOperate = hasSomePermission(['sys:position:update', 'sys:position:delete']); +``` + +## useLockFn + +防重复提交 Hook,函数执行期间自动锁定,位于 `src/utils/useLockFn.ts`: + +```typescript +// src/utils/useLockFn.ts +import { ref } from 'vue'; + +export function useLockFn(fn: (...args: any[]) => Promise) { + const isLock = ref(false); + const lockFn = async (...args: any[]) => { + if (isLock.value) return; // 锁定期间忽略调用 + isLock.value = true; + try { + await fn(...args); + } finally { + isLock.value = false; // 无论成功失败都解锁 + } + }; + return { isLock, lockFn }; +} +``` + +实际用法(edit.vue 中的提交按钮): + +```typescript +// src/views/system/position/edit.vue +import { useLockFn } from '@/utils/useLockFn'; + +const handleSubmit = async () => { + await formRef.value?.validate(); + props.positionId ? await positionUpdate(formData) : await positionAdd(formData); + message('操作成功'); + emit('update:visible', false); + emit('success'); +}; + +// isLock 绑定按钮的 loading 状态,lockFn 作为点击处理函数 +const { isLock: subLoading, lockFn: submit } = useLockFn(handleSubmit); +``` + +```vue +确定 +``` + +## useAsync + +异步操作 loading 管理,位于 `src/hooks/use-async.ts`: + +```typescript +// src/hooks/use-async.ts +import { isReactive, isRef } from 'vue'; + +function setLoading(loading, val) { + if (loading != undefined && isRef(loading)) { + loading.value = val; + } else if (loading != undefined && isReactive(loading)) { + loading.loading = val; + } +} + +export const useAsync = async (func: Promise, loading: any): Promise => { + setLoading(loading, true); + return await func.finally(() => setLoading(loading, false)); +}; +``` + +使用示例: + +```typescript +const loading = ref(false); +const fetchData = async () => { + await useAsync(getPositionList(params), loading); +}; +``` + +## useTime + +实时时钟 Hook,每秒更新当前时间,位于 `src/hooks/useTime.ts`: + +```typescript +// src/hooks/useTime.ts +import { ref, onMounted, onUnmounted } from 'vue'; + +export function useTime() { + let timer; + const year = ref(0); + const month = ref(0); + const week = ref(''); + const day = ref(0); + const hour = ref(0); + const minute = ref(0); + const second = ref(0); + + const updateTime = () => { + const date = new Date(); + year.value = date.getFullYear(); + month.value = date.getMonth() + 1; + week.value = '日一二三四五六'.charAt(date.getDay()); + day.value = date.getDate(); + hour.value = (date.getHours() + '')?.padStart(2, '0'); + minute.value = (date.getMinutes() + '')?.padStart(2, '0'); + second.value = date.getSeconds(); + }; + + updateTime(); + onMounted(() => { timer = setInterval(() => updateTime(), 1000); }); + onUnmounted(() => { clearInterval(timer); }); + + return { month, day, hour, minute, second, week }; +} +``` + +## useProjectSetting + +项目配置 Hook,读取项目全局设置: + +```typescript +// src/hooks/setting/useProjectSetting.ts +export function useProjectSetting() { + const projectStore = useProjectSettingStore(); + return { + getPermissionMode: computed(() => projectStore.permissionMode), // 'BACK' | 'FRONT' + getIsWaterMark: computed(() => projectStore.isWaterMark), + // ... 其他配置 + }; +} +``` + +## useDesignSetting + +设计配置 Hook,管理主题色和深色模式: + +```typescript +// src/hooks/setting/useDesignSetting.ts +export function useDesignSetting() { + return { + getAppTheme: computed(() => designStore.appTheme), + getDarkTheme: computed(() => designStore.darkTheme), + // ... 其他设计配置 + }; +} +``` + +## useWindowSizeFn + +窗口大小变化回调 Hook,用于表格高度自适应: + +```typescript +// src/hooks/event/useWindowSizeFn.ts + +export function useWindowSizeFn(callback: Fn, wait?: number) { + // 监听 window.resize 事件,防抖执行回调 +} +``` + +BasicTable 组件中使用此 Hook 自动计算表格高度: + +```typescript +// src/components/Table/src/Table.vue +useWindowSizeFn(computeTableHeight, 280); +``` + +:::tip 温馨提示 +Hooks 是 Vue3 组合式 API 的核心模式,将可复用逻辑抽离为函数。`usePermission` 供 `v-perm` 指令和 `v-if` 条件使用,`useLockFn` 防止按钮重复点击,`useAsync` 统一管理 loading 状态。 +::: + +## 总结 + +项目提供了 usePermission(权限检查)、useLockFn(防重复提交)、useAsync(异步 loading)、useTime(实时时钟)、useProjectSetting(项目配置)、useDesignSetting(设计配置)、useWindowSizeFn(窗口变化)等组合式函数,将常用逻辑封装为可复用的函数,避免 Mixin 的命名冲突问题。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/i18n.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/i18n.md new file mode 100644 index 0000000..a3c6a36 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/i18n.md @@ -0,0 +1,150 @@ +# 国际化 + +:::tip 说明 +当前项目仅支持中文界面,ElementPlus 使用中文语言包(`zh-cn`),所有界面文本直接硬编码在组件中。如需扩展多语言支持,可基于 vue-i18n 实现。 +::: + +## 当前状态 + +项目当前为中文单语言版本: + +- ElementPlus 全局配置中文语言包:`app.use(ElementPlus, { locale: zhCn })` +- 所有界面文本直接硬编码在组件中:`添加岗位` +- 后端返回的 `msg` 字段为中文:`"操作成功"`、`"岗位名称不能重复"` + +```vue + +``` + +## 扩展方案 + +如需支持多语言,推荐使用 vue-i18n: + +### 安装依赖 + +```bash +pnpm add vue-i18n +``` + +### 配置 i18n + +```typescript +// src/i18n/index.ts +import { createI18n } from 'vue-i18n'; +import zhCN from './locales/zh-CN.json'; +import enUS from './locales/en-US.json'; + +const i18n = createI18n({ + legacy: false, + locale: localStorage.getItem('lang') || 'zh-CN', + fallbackLocale: 'zh-CN', + messages: { 'zh-CN': zhCN, 'en-US': enUS }, +}); + +export default i18n; +``` + +### 语言文件 + +```json +// src/i18n/locales/zh-CN.json +{ + "common": { + "add": "新增", "edit": "编辑", "delete": "删除", + "search": "搜索", "reset": "重置", "confirm": "确定", "cancel": "取消" + }, + "position": { "name": "岗位名称", "status": "状态", "sort": "排序" } +} +``` + +### 组件中使用 + +```vue + + + +``` + +### ElementPlus 国际化 + +```typescript +import zhCn from 'element-plus/es/locale/lang/zh-cn'; +import en from 'element-plus/es/locale/lang/en'; + +app.use(ElementPlus, { locale: zhCn }); +``` + +### 语言切换组件 + +```vue + + + + +``` + +### 后端消息国际化 + +后端返回的 `msg` 字段可通过 code 映射实现多语言: + +```typescript +// 前端根据 code 映射多语言消息 +const msgMap: Record = { + 0: t('common.success'), + 1: t('common.failed'), + 401: t('common.unauthorized'), + 403: t('common.forbidden'), +}; + +// Axios 响应拦截器中 +if (response.data.code !== 0) { + const msg = msgMap[response.data.code] || response.data.msg; + ElMessage.error(msg); +} +``` + +:::tip 温馨提示 +国际化改造工作量较大,建议在项目初期规划。如仅需中文,当前硬编码方式即可满足需求。改造时需要同时处理前端文本和后端返回的 `msg` 字段。 +::: + +## 总结 + +当前项目为中文单语言版本,ElementPlus 使用中文语言包。如需扩展多语言支持,可基于 vue-i18n 实现前端国际化,配合 ElementPlus 内置语言包和后端消息 code 实现全链路多语言。语言切换组件可放在顶栏,支持用户手动切换。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/modal.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/modal.md new file mode 100644 index 0000000..02450d9 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/modal.md @@ -0,0 +1,216 @@ +# Modal 弹窗 + +:::tip 说明 +项目提供 `BasicModal` 组件(`src/components/Modal/src/BasicModal.vue`)和原生 `el-dialog` 两种弹窗用法。编辑弹窗通过 `defineAsyncComponent` 懒加载,减少首屏渲染负担。 +::: + +## 组件架构 + +``` +src/components/Modal/ +├── src/ +│ ├── BasicModal.vue # 主组件(基于 el-dialog) +│ ├── props.ts # 属性定义 +│ ├── type.ts # 类型定义(ModalProps、ModalMethods) +│ └── hooks/ +│ └── useModalContext.ts # 弹窗上下文 +└── index.ts # 导出 useModal 等 +``` + +## BasicModal 组件 + +`BasicModal` 基于 `el-dialog` 封装,提供全屏切换、自定义头部、提交按钮等能力: + +```vue + + + +``` + +核心功能: + +- 自定义头部(标题 + 全屏/关闭按钮) +- 提交按钮自动 loading(`setSubLoading`) +- `setProps` 动态修改弹窗属性 +- `openModal` / `closeModal` 控制显隐 + +## 实际用法(position 模块) + +以岗位管理为例,实际使用原生 `el-dialog` + `defineAsyncComponent` 懒加载: + +### 父组件(index.vue) + +```vue + + + + +``` + +`defineAsyncComponent` 实现懒加载:首屏不加载弹窗组件,首次打开时才加载,加载完成后缓存。 + +### 编辑弹窗(edit.vue) + +```vue + + + + +``` + +## 弹窗数据流 + +``` +父组件点击编辑 → positionId = record.row.id → editVisible = true + ↓ +edit.vue onMounted → positionId != 0 → setFormData() → 获取详情填充表单 + ↓ +用户修改 → submit → useLockFn(防重复) → formRef.validate() → positionUpdate(formData) + ↓ +emit('success') → 父组件 reloadTable() → 表格刷新 +``` + +## 确认弹窗 + +删除等危险操作使用 `confirm` 封装函数(来自 `src/utils/auth.ts`): + +```typescript +import { message, confirm } from '@/utils/auth'; + +async function handleDelete(record: Recordable) { + let ids = []; + if (!record) { + ids = selectionData.value.map(({ id }) => id); + } + + await confirm('确定要删除?'); + record ? await positionDelete(record.row.id) : await positionBatchDelete(ids); + message('删除成功'); + reloadTable(); +} +``` + +`confirm` 内部调用 `ElMessageBox.confirm`,返回 Promise,用户取消时抛出异常中断后续逻辑。 + +## v-model:visible 双向绑定 + +弹窗使用 `v-model:visible` 实现父子组件双向绑定: + +```vue + + + + + +``` + +:::tip 温馨提示 +弹窗组件通过 `defineAsyncComponent(() => import('./edit.vue'))` 懒加载,首屏不加载弹窗代码。`useLockFn` 将 `isLock` 绑定按钮的 `:loading` 属性,防止重复提交。关闭弹窗时通过 `v-model:visible` 自动同步父子状态。 +::: + +## 总结 + +编辑弹窗使用 `el-dialog` + `defineAsyncComponent` 懒加载,通过 `v-model:visible` 双向绑定显隐状态,`useLockFn` 防重复提交。父组件通过 `positionId` prop 传递编辑行 ID,子组件在 `onMounted` 中判断并加载详情数据。操作成功后 `emit('success')` 通知父组件刷新表格。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/perm-directive.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/perm-directive.md new file mode 100644 index 0000000..ae83e17 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/perm-directive.md @@ -0,0 +1,227 @@ +# 权限指令 v-perm + +:::tip 说明 +`v-perm` 和 `v-perms` 是项目自定义的 Vue 指令,用于按钮级别的权限控制,无权限时自动从 DOM 中移除元素。底层通过 `usePermission` Hook 检查用户权限列表。 +::: + +## 指令定义 + +权限指令定义在 `src/directives/permission.ts` 中: + +```typescript +// src/directives/permission.ts + +import { ObjectDirective } from 'vue'; +import { usePermission } from '@/hooks/web/usePermission'; + +// v-perm:满足任一权限即可显示 +export const perm: ObjectDirective = { + mounted(el: HTMLButtonElement, binding) { + if (binding.value == undefined) return; + const { hasSomePermission } = usePermission(); + if (!hasSomePermission(binding.value)) { + el.parentNode.removeChild(el); + } + }, +}; + +// v-perms:必须满足所有权限才显示 +export const perms: ObjectDirective = { + mounted(el: HTMLButtonElement, binding) { + if (binding.value == undefined) return; + const { hasEveryPermission } = usePermission(); + if (!hasEveryPermission(binding.value)) { + el.parentNode.removeChild(el); + } + }, +}; +``` + +指令在 `mounted` 钩子中检查当前用户的权限列表,如果不满足权限条件,直接从 DOM 中移除元素。 + +## usePermission Hook + +权限检查的核心逻辑在 `src/hooks/web/usePermission.ts` 中: + +```typescript +// src/hooks/web/usePermission.ts + +import { useUserStore } from '@/store/modules/user'; + +export function usePermission() { + const userStore = useUserStore(); + + function _somePermissions(accesses: string[]) { + const permissionsList = userStore.getPermissions; + if (permissionsList[0] == '*:*:*') return true; // 超级管理员通配符 + return accesses.some((item) => permissionsList.includes(item)); + } + + // 是否包含其中某个权限 + function hasSomePermission(accesses?: string | string[]): boolean { + const permissionsList = userStore.getPermissions; + if (permissionsList[0] === '*:*:*') return true; + const list = Array.isArray(accesses) ? accesses : []; + return list.some((access) => permissionsList.includes(access)); + } + + // 是否包含指定的所有权限 + function hasEveryPermission(accesses?: string | string[]): boolean { + const list = Array.isArray(accesses) ? accesses : []; + const permissionsList = userStore.getPermissions; + if (permissionsList[0] === '*:*:*') return true; + return list.every((access) => permissionsList.includes(access)); + } + + // 判断是否存在权限(可用于 v-if) + function hasPermission(accesses: string[]): boolean { + if (!accesses || !accesses.length) return true; + return _somePermissions(accesses); + } + + return { hasPermission, hasEveryPermission, hasSomePermission }; +} +``` + +权限列表在登录后通过 `userStore.GetInfo()` 从后端获取,存储在 Pinia 的 user store 中。 + +## 基本用法 + +### v-perm(满足任一权限) + +```vue +添加岗位 +删除 +导出 +``` + +### v-perms(满足所有权限) + +```vue +高级操作 +``` + +### 与 v-if 结合 + +```typescript +import { usePermission } from '@/hooks/web/usePermission'; + +const { hasPermission } = usePermission(); + +// 在模板中使用 +const showAddBtn = computed(() => hasPermission(['sys:position:add'])); +``` + +```vue +添加岗位 +``` + +## 实际用法(position 模块) + +以岗位管理页面为例,实际代码如下: + +```vue + + +``` + +操作列中的权限控制通过 `auth` 属性实现: + +```typescript +const actionColumn = reactive({ + width: 200, + label: '操作', + fixed: 'right', + render(record) { + return h(TableAction, { + style: 'button', + actions: [ + { + label: '编辑', + icon: 'Edit', + type: 'warning', + onClick: handleEdit.bind(null, record), + auth: ['sys:position:update'], // 权限节点 + }, + { + label: '删除', + icon: 'Delete', + type: 'danger', + onClick: handleDelete.bind(null, record), + auth: ['sys:position:delete'], // 权限节点 + }, + ], + }); + }, +}); +``` + +## 权限节点命名规范 + +权限节点格式:`sys:{module}:{action}` + +``` +sys:position:page —— 分页查询 +sys:position:detail —— 查询详情 +sys:position:list —— 获取列表 +sys:position:add —— 新增 +sys:position:update —— 编辑 +sys:position:delete —— 删除 +sys:position:batchDelete —— 批量删除 +sys:position:status —— 设置状态 +sys:position:export —— 导出 +sys:position:import —— 导入 +``` + +## 与后端对应 + +前端 `v-perm` 指令的权限节点必须与后端 `@permission_required` 装饰器的权限字符串一致: + +```python +# 后端 +class PositionAddHandler(BaseHandler): + @permission_required("sys:position:add") + @check_demo + async def post(self): + return await position_service.add(self) +``` + +```vue + +添加岗位 +``` + +## 超级管理员 + +用户权限列表中包含 `*:*:*` 通配符时,所有权限检查直接返回 `true`,等同于后端用户 ID=1 的管理员跳过权限检查。 + +:::tip 温馨提示 +`v-perm` 指令仅做 UI 层隐藏,真正的权限校验在后端 `@permission_required` 装饰器中。即使前端绕过指令限制直接调用接口,后端仍会校验权限。 +::: + +## 指令注册 + +指令在 `src/plugins/` 中通过 `setupDirectives` 全局注册: + +```typescript +// 启动时注册(main.ts) +setupDirectives(app); // 注册 v-perm、v-perms 等指令 +``` + +## 总结 + +`v-perm`(满足任一权限)和 `v-perms`(满足所有权限)指令通过 `usePermission` Hook 检查用户权限列表,无权限时自动从 DOM 移除元素。操作列通过 `TableAction` 组件的 `auth` 属性控制。权限节点在菜单管理中配置,需要保持前后端字符串完全一致。`*:*:*` 通配符表示超级管理员。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/router.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/router.md new file mode 100644 index 0000000..8c10009 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/router.md @@ -0,0 +1,304 @@ +# 路由与菜单 + +:::tip 说明 +前端路由基于 Vue Router 4 实现动态路由生成,菜单数据从后端获取,通过 `generator-routers.ts` 递归生成路由表,路由守卫控制登录状态和权限。 +::: + +## 路由配置 + +路由配置位于 `src/router/` 目录: + +``` +src/router/ +├── index.ts # 路由实例 +├── router-guards.ts # 路由守卫 +├── generator-routers.ts # 动态路由生成 +├── router-icons.ts # 菜单图标映射 +├── constant.ts # 常量路由 +└── base.ts # 静态路由 +``` + +### 静态路由 + +无需权限即可访问的路由定义在 `base.ts` 中,包括登录页、404 错误页等。 + +### 动态路由生成 + +登录后从后端获取菜单数据,由 `generator-routers.ts` 的 `routerGenerator` 递归生成路由表: + +```typescript +// src/router/generator-routers.ts + +import { adminMenus } from '@/api/common/menu'; +import { Layout, ParentLayout } from '@/router/constant'; + +const LayoutMap = new Map Promise>(); +LayoutMap.set('LAYOUT', Layout); +LayoutMap.set('IFRAME', () => import('@/views/iframe/index.vue')); + +export const routerGenerator = (routerMap, parent?): any[] => { + return routerMap.map((item) => { + const names = item.target == 2 ? item.component : item.path.replaceAll('/', ''); + item.meta = { + title: item.parentId == 0 && item.children.length == 0 ? '' : item.name, + icon: item.icon, + sort: item.sort, + permissions: item.permission, + hidden: item.hide ? true : false, + isRoot: item.parentId == 0 && item.children.length == 0, + frameSrc: item.target == 1 ? item.component : '', + }; + + let components = ''; + if (item.parentId == 0 && item.children.length == 0) { + components = 'LAYOUT'; + } else if (item.target == 0) { + components = item.component; + } else if (item.target == 1) { + components = 'IFRAME'; + } + + const currentRouter: any = { + path: item.target != 2 ? item.path : '', + name: names, + component: components, + meta: { ...item.meta, label: item.meta.title, icon: item.meta.icon || null }, + }; + + if (item.children && item.children.length > 0) { + !item.redirect && (currentRouter.redirect = `${item.children[0].path}`); + currentRouter.children = routerGenerator(item.children, currentRouter); + } + + return currentRouter; + }); +}; +``` + +### 组件动态导入 + +`asyncImportRoute` 函数使用 `import.meta.glob` 扫描 `views/` 目录下的所有 `.vue` 和 `.tsx` 文件,根据后端返回的 `component` 字段(如 `system/position/index`)匹配并懒加载: + +```typescript +export const asyncImportRoute = (routes: AppRouteRecordRaw[]): void => { + viewsModules = viewsModules || import.meta.glob('../views/**/*.{vue,tsx}'); + routes.forEach((item) => { + const { component } = item; + if (component) { + const layoutFound = LayoutMap.get(component as string); + if (layoutFound) { + item.component = layoutFound; // LAYOUT / IFRAME + } else { + item.component = dynamicImport(viewsModules, component as string); + } + } + item.children && asyncImportRoute(item.children); + }); +}; + +export const dynamicImport = (viewsModules, component: string) => { + const keys = Object.keys(viewsModules); + const matchKeys = keys.filter((key) => { + let k = key.replace('../views', ''); + k = k.substring(0, k.lastIndexOf('.')); + return k === component; + }); + if (matchKeys?.length === 1) return viewsModules[matchKeys[0]]; +}; +``` + +匹配规则:后端 `component` 字段值 `system/position/index` 会匹配 `../views/system/position/index.vue`。 + +## 路由守卫 + +`router-guards.ts` 中定义全局路由守卫,核心流程: + +```typescript +// src/router/router-guards.ts + +export function createRouterGuards(router: Router) { + const userStore = useUserStoreWidthOut(); + const asyncRouteStore = useAsyncRouteStoreWidthOut(); + + router.beforeEach(async (to, from, next) => { + // 支持 URL 参数中的 token(如 SSO 回调) + if (to.query && 'token' in to.query) { + storage.set(ACCESS_TOKEN, String(to.query.token)); + userStore.setToken(String(to.query.token)); + } + NProgress.start(); + + const token = storage.get(ACCESS_TOKEN); + + // 白名单(登录页)直接放行 + if (whitePathList.includes(to.path) && !token) { + next(); return; + } + + // 无 token 跳转登录页 + if (!token) { + if (to.meta.ignoreAuth) { next(); return; } + next({ path: LOGIN_PATH, replace: true, query: { redirect: to.path } }); + return; + } + + // 已登录且路由已添加,直接放行 + if (asyncRouteStore.getIsDynamicAddedRoute) { + next(); return; + } + + // 首次登录:获取用户信息 → 生成动态路由 → 添加路由 + asyncRouteStore.setDynamicAddedRoute(true); + const userInfo = await userStore.GetInfo(); + const routes = await asyncRouteStore.generateRoutes(userInfo); + + routes.forEach((item) => { + router.addRoute(item as unknown as RouteRecordRaw); + }); + + // 添加 404 页面 + const isErrorPage = router.getRoutes().findIndex((item) => item.name === ErrorPageRoute.name); + if (isErrorPage === -1) { + router.addRoute(ErrorPageRoute as unknown as RouteRecordRaw); + } + + const firstRoutePath = findFirstRoutePath(routes); + storage.set(FIRST_ROUTE, firstRoutePath); + + // 跳转目标页或首个路由 + next({ path: redirect, replace: true }); + }); + + // afterEach:设置页面标题、管理 keepAlive 组件缓存 + router.afterEach((to) => { + document.title = to?.meta?.title || document.title; + NProgress.done(); + }); +} +``` + +守卫流程图: + +``` +访问页面 + ├── 有 token 查询参数?→ 存储 token + ├── 白名单?→ 放行 + ├── 无 token?→ 跳转登录页(携带 redirect 参数) + ├── 路由已添加?→ 放行 + └── 首次登录 → GetInfo → generateRoutes → addRoute → 跳转 +``` + +## 动态路由生成(asyncRoute Store) + +`asyncRouteStore` 的 `generateRoutes` 方法负责从后端获取菜单并生成路由: + +```typescript +// src/store/modules/asyncRoute.ts + +async generateRoutes(data) { + let accessedRouters; + const permissionsList = data.permissions || []; + const { getPermissionMode } = useProjectSetting(); + const permissionMode = unref(getPermissionMode); + + if (permissionMode === 'BACK') { + // 后端控制模式:调用 adminMenus() 获取菜单 + accessedRouters = await generatorDynamicRouter(); + } else { + // 前端控制模式:根据权限过滤静态路由表 + accessedRouters = filter(asyncRoutes, routeFilter); + } + + this.setRouters(accessedRouters); + this.setMenus(accessedRouters); + return toRaw(accessedRouters); +}, +``` + +使用 `BACK` 模式(后端控制菜单),调用 `adminMenus()` 接口获取菜单数据。 + +## 菜单数据结构 + +后端返回的菜单数据结构: + +```json +[ + { + "id": 1, + "name": "系统管理", + "path": "/system", + "component": "", + "icon": "icon-system", + "target": 0, + "parentId": 0, + "sort": 1, + "hide": false, + "permission": "", + "children": [ + { + "id": 10, + "name": "岗位管理", + "path": "/system/position", + "component": "system/position/index", + "icon": "icon-position", + "target": 0, + "parentId": 1, + "sort": 3, + "hide": false, + "permission": "sys:position:page", + "children": [] + } + ] + } +] +``` + +| 字段 | 说明 | +|------|------| +| `name` | 菜单名称,同时作为 `meta.title` | +| `path` | 路由地址,如 `/system/position` | +| `component` | 组件路径,如 `system/position/index`,对应 `views/system/position/index.vue` | +| `target` | 打开方式:0=内部组件,1=内嵌 iframe,2=外部链接 | +| `parentId` | 父菜单 ID,0 表示顶级 | +| `permission` | 权限节点,如 `sys:position:page` | +| `hide` | 是否隐藏菜单 | +| `icon` | 菜单图标 | + +## 菜单渲染 + +菜单数据与路由数据同源,侧边栏组件递归渲染菜单树。`meta` 中的关键字段控制渲染行为: + +- `hidden: true`:隐藏该菜单项 +- `isRoot: true`:顶级单页菜单(无子菜单的顶级项自动包裹 Layout) +- `frameSrc`:iframe 内嵌地址 +- `permissions`:权限字符串,用于菜单级别的权限过滤 + +## 面包屑导航 + +根据当前路由的 `matched` 自动生成面包屑,读取每级路由的 `meta.title`。 + +## KeepAlive 缓存 + +路由守卫的 `afterEach` 中自动管理组件缓存: + +```typescript +router.afterEach((to) => { + const keepAliveComponents = asyncRouteStore.keepAliveComponents; + const currentComName = to.matched.find((item) => item.name == to.name)?.name; + + if (currentComName && !keepAliveComponents.includes(currentComName) && to.meta?.keepAlive) { + keepAliveComponents.push(currentComName); // 需要缓存 + } else if (!to.meta?.keepAlive) { + const index = keepAliveComponents.findIndex((name) => name == currentComName); + if (index != -1) keepAliveComponents.splice(index, 1); // 移除缓存 + } +}); +``` + +:::tip 温馨提示 +菜单的 `component` 字段对应 `src/views/` 下的组件路径,如 `system/position/index` 对应 `src/views/system/position/index.vue`。`import.meta.glob('../views/**/*.{vue,tsx}')` 会在构建时扫描所有视图文件,实现按需懒加载。 +::: + +## 总结 + +前端路由采用动态生成方式,登录后通过 `adminMenus()` 接口获取后端菜单数据,`routerGenerator` 递归生成路由表,`asyncImportRoute` 使用 `import.meta.glob` 懒加载组件。路由守卫控制登录状态和首次路由添加,`asyncRouteStore` 管理路由状态和组件缓存。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/setup.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/setup.md new file mode 100644 index 0000000..3bdf023 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/setup.md @@ -0,0 +1,215 @@ +# 前端环境与结构 + +:::tip 说明 +前端基于 Vue3 + ElementPlus 构建,使用 pnpm 作为包管理工具,Vite 作为构建工具,TypeScript 作为开发语言。 +::: + +## 技术栈 + +| 技术 | 版本 | 说明 | +|------|------|------| +| Vue | 3.x | 渐进式 JavaScript 框架,组合式 API | +| ElementPlus | 2.x | Vue3 组件库 | +| Vite | 3.x | 下一代前端构建工具 | +| Pinia | 2.x | 状态管理(Vuex 替代方案) | +| Vue Router | 4.x | 路由管理 | +| Axios | 1.x | HTTP 请求库 | +| TypeScript | 5.x | 类型安全的 JavaScript 超集 | +| Tailwind CSS | 3.x | 原子化 CSS 框架 | +| SCSS | - | CSS 预处理器 | + +## 目录结构 + +``` +ui/ +├── src/ +│ ├── api/ # API 接口定义 +│ │ ├── common/ # 公共接口(login、menu、user) +│ │ └── system/ # 系统模块接口 +│ ├── assets/ # 静态资源 +│ ├── components/ # 公共组件 +│ ├── directives/ # 自定义指令 +│ ├── hooks/ # 组合式函数 +│ │ ├── event/ # 事件 hooks +│ │ ├── setting/ # 配置 hooks +│ │ └── web/ # Web hooks +│ ├── layout/ # 布局组件 +│ ├── router/ # 路由配置 +│ ├── store/ # 状态管理 +│ │ └── modules/ # Store 模块 +│ ├── styles/ # 全局样式 +│ ├── utils/ # 工具函数 +│ │ └── http/axios/ # Axios 封装 +│ └── views/ # 页面视图 +│ └── system/ # 系统管理页面(position、user、role 等) +├── build/ # 构建配置(vite 插件、常量、工具函数) +├── public/ # 公共静态资源 +├── vite.config.ts # Vite 配置 +├── tsconfig.json # TypeScript 配置 +└── package.json # 依赖配置 +``` + +## 启动开发 + +```bash +# 安装依赖 +pnpm install + +# 启动开发服务器(默认端口 8001) +pnpm dev + +# 构建生产版本 +pnpm build +``` + +## Vite 配置 + +`vite.config.ts` 是 Vite 的核心配置文件,实际配置如下: + +```typescript +import type { UserConfig, ConfigEnv } from 'vite'; +import { loadEnv } from 'vite'; +import { resolve } from 'path'; +import { wrapperEnv } from './build/utils'; +import { createVitePlugins } from './build/vite/plugin'; +import { OUTPUT_DIR } from './build/constant'; +import { createProxy } from './build/vite/proxy'; + +export default ({ command, mode }: ConfigEnv): UserConfig => { + const root = process.cwd(); + const env = loadEnv(mode, root); + const viteEnv = wrapperEnv(env); + const { VITE_PUBLIC_PATH, VITE_PORT, VITE_GLOB_PROD_MOCK } = viteEnv; + + let VITE_PROXY: any = viteEnv.VITE_PROXY; + // VITE_PROXY 容错处理 + if (!VITE_PROXY) { + VITE_PROXY = '[[]]'; + } + try { + if (typeof VITE_PROXY === 'string') { + VITE_PROXY = JSON.parse(VITE_PROXY); + } + } catch (e) { + VITE_PROXY = [[]]; + } + + const isBuild = command === 'build'; + + return { + base: VITE_PUBLIC_PATH, + resolve: { + alias: [ + { find: '@', replacement: pathResolve('src') + '/' }, + ], + dedupe: ['vue'], + }, + plugins: createVitePlugins(viteEnv, isBuild, prodMock), + css: { + preprocessorOptions: { + scss: { + additionalData: `@import "src/styles/var.scss";`, + }, + }, + }, + server: { + host: true, + port: VITE_PORT, + proxy: createProxy(VITE_PROXY), + }, + build: { + target: 'es2015', + outDir: OUTPUT_DIR, + reportCompressedSize: false, + chunkSizeWarningLimit: 2000, + }, + }; +}; +``` + +关键配置说明: + +- `@` 别名指向 `src/` 目录,代码中可用 `@/api/system/position` 引入模块 +- SCSS 全局变量通过 `additionalData` 注入,无需每个文件手动 `@import` +- 代理配置通过 `VITE_PROXY` 环境变量传入,由 `createProxy()` 工具函数解析 +- 构建产物输出到 `OUTPUT_DIR`(默认 `dist/`) + +## 应用入口 + +`src/main.ts` 是应用入口,负责初始化所有基础设施: + +```typescript +import './styles/tailwindbase.css'; +import 'element-plus/dist/index.css'; +import './styles/index.scss'; +import * as ElementPlusIconsVue from '@element-plus/icons-vue'; +import { createApp } from 'vue'; +import App from './App.vue'; +import router, { setupRouter } from './router'; +import { setupStore } from '@/store'; +import { setupElement, setupDirectives, setupCustomComponents } from '@/plugins'; + +async function bootstrap() { + const app = createApp(App); + + // 注册所有 ElementPlus 图标 + for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component); + } + + setupElement(app); // 全局引入 ElementPlus 组件 + setupCustomComponents(app); // 注册自定义组件 + setupDirectives(app); // 注册自定义指令(v-perm 等) + setupStore(app); // 挂载 Pinia 状态管理 + await setupRouter(app); // 挂载路由 + await router.isReady(); // 路由准备就绪 + + app.mount('#app', true); +} + +void bootstrap(); +``` + +初始化顺序:样式加载 -> ElementPlus -> 自定义组件 -> 自定义指令 -> Pinia Store -> Router -> 挂载。 + +## 核心目录说明 + +### api/ + +API 接口定义目录,按模块组织文件。公共接口在 `api/common/`(login、menu、user),业务接口在 `api/system/`(position、user、role、dept、level、menu、dictionary 等)。每个文件使用 `http.request()` 发起请求。 + +### components/ + +公共组件目录,基于 ElementPlus 二次封装,包含 `Table`(BasicTable 表格)、`Form`(BasicForm 表单)、`Modal`(BasicModal 弹窗)、`Upload`、`Editor`、`Cropper`、`Select` 等。 + +### directives/ + +自定义指令目录,核心是 `permission.ts` 中的 `v-perm` 和 `v-perms` 指令,用于按钮级别的权限控制。 + +### hooks/ + +组合式函数目录,包括 `usePermission`(权限检查)、`useTime`(实时时钟)、`useAsync`(异步 loading)、`useProjectSetting`(项目配置)、`useDesignSetting`(设计配置)等。 + +### store/ + +Pinia 状态管理目录,核心模块: + +- `user`:用户登录状态、Token、用户信息、权限列表 +- `asyncRoute`:动态路由生成、菜单管理、组件缓存 +- `projectSetting`:项目配置(权限模式、水印等) +- `designSetting`:主题色、深色模式 +- `lockscreen`:锁屏状态 +- `tabsView`:标签页管理 + +### utils/ + +工具函数目录: + +- `http/axios/`:Axios 封装(VAxios 类、请求/响应拦截器、取消重复请求、状态码处理) +- `auth.ts`:认证工具(token 管理、树结构操作、文件下载) +- `Storage.ts`:本地存储封装(支持过期时间、cookie) +- `dateUtil.ts`:日期格式化(基于 dayjs) + +## 总结 + +前端项目基于 Vue3 + ElementPlus + Vite + TypeScript + Pinia 构建,使用 pnpm 管理依赖,Tailwind CSS + SCSS 提供样式能力。通过 `main.ts` 的 `bootstrap()` 函数按顺序初始化所有基础设施,目录结构按功能分层组织,便于维护和扩展。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/store.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/store.md new file mode 100644 index 0000000..6843c8a --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/store.md @@ -0,0 +1,245 @@ +# Store 状态管理 + +:::tip 说明 +前端使用 Pinia 进行状态管理,核心模块包括 `user`(用户信息与权限)、`asyncRoute`(动态路由与菜单)、`projectSetting`(项目配置)、`designSetting`(主题设计)等。 +::: + +## Pinia 配置 + +```typescript +// src/store/index.ts +import { createPinia } from 'pinia'; +const pinia = createPinia(); +export default pinia; +``` + +Store 模块位于 `src/store/modules/` 目录: + +``` +src/store/modules/ +├── user.ts # 用户登录状态、Token、用户信息、权限列表 +├── asyncRoute.ts # 动态路由生成、菜单管理、组件缓存 +├── projectSetting.ts # 项目配置(权限模式、水印、动画等) +├── designSetting.ts # 主题色、深色模式 +├── lockscreen.ts # 锁屏状态 +├── tabsView.ts # 标签页管理 +└── ossConfig.ts # OSS 配置 +``` + +## User Store + +管理用户登录状态、Token、用户信息和权限列表,是核心 Store: + +```typescript +// src/store/modules/user.ts + +import { defineStore } from 'pinia'; +import { createStorage } from '@/utils/Storage'; +import { ACCESS_TOKEN, CURRENT_USER } from '@/store/mutation-types'; +import { getUserInfo, login, logout } from '@/api/common/user'; + +const Storage = createStorage({ storage: localStorage }); + +export interface IUserState { + token: string; + username: string; + welcome: string; + avatar: string; + permissions: any[]; + info: any; +} + +export const useUserStore = defineStore({ + id: 'app-user', + state: (): IUserState => ({ + token: Storage.get(ACCESS_TOKEN, ''), // 从 localStorage 恢复 token + username: '', + welcome: '', + avatar: '', + permissions: [], + info: Storage.get(CURRENT_USER, {}), // 从 localStorage 恢复用户信息 + }), + getters: { + getToken(): string { return this.token; }, + getPermissions(): [any][] { return this.permissions; }, + getUserInfo(): object { return this.info; }, + }, + actions: { + // 登录 + async login(userInfo) { + const response = await login(userInfo); + const { code, data } = response; + if (code === ResultEnum.SUCCESS) { + const ex = 7 * 24 * 60 * 60 * 1000; // 7 天过期 + storage.set(ACCESS_TOKEN, data.access_token, ex); + this.setToken(data.access_token); + } + return Promise.resolve(response); + }, + + // 获取用户信息 + GetInfo() { + return new Promise((resolve, reject) => { + getUserInfo().then((res) => { + if (res.permissions && res.permissions.length) { + this.setPermissions(res.permissions); + } + this.setUserInfo(res); + this.setAvatar(res.avatar); + resolve(res); + }).catch(reject); + }); + }, + + // 登出 + async logout() { + return new Promise((resolve, reject) => { + logout().then(() => { + this.setPermissions([]); + this.setUserInfo(''); + storage.remove(ACCESS_TOKEN); + storage.remove(CURRENT_USER); + storage.clear(); + resolve(''); + }).catch(reject); + }); + }, + }, +}); +``` + +### Token 存储机制 + +Token 通过 `Storage` 工具类存储在 `localStorage` 中,键名为 `ACCESS-TOKEN`,自动大写转换,支持 7 天过期时间: + +```typescript +// src/utils/Storage.ts +export const createStorage = ({ prefixKey = '', storage = localStorage } = {}) => { + const Storage = class { + set(key: string, value: any, expire: number | null = DEFAULT_CACHE_TIME) { + const stringData = JSON.stringify({ + value, + expire: expire !== null ? Date.now() + expire : null, + }); + this.storage.setItem(this.getKey(key), stringData); + } + + get(key: string, def: any = null) { + const item = this.storage.getItem(this.getKey(key)); + if (item) { + const data = JSON.parse(item); + const { value, expire } = data; + if (expire === null || expire >= Date.now()) return value; + this.remove(key); // 过期则删除 + } + return def; + } + }; + return new Storage(); +}; +export const storage = createStorage(); +``` + +## AsyncRoute Store + +管理动态路由和菜单,是路由系统的核心: + +```typescript +// src/store/modules/asyncRoute.ts +export const useAsyncRouteStore = defineStore({ + id: 'app-async-route', + state: (): IAsyncRouteState => ({ + menus: [], // 菜单列表 + routers: constantRouter, // 静态路由 + addRouters: [], // 动态路由 + keepAliveComponents: [], // 需要缓存的组件名 + isDynamicAddedRoute: false, // 路由是否已添加 + }), + actions: { + async generateRoutes(data) { + const { getPermissionMode } = useProjectSetting(); + const permissionMode = unref(getPermissionMode); + let accessedRouters; + + if (permissionMode === 'BACK') { + // 后端控制模式:从后端获取菜单生成路由 + accessedRouters = await generatorDynamicRouter(); + } else { + // 前端控制模式:根据权限过滤静态路由表 + accessedRouters = filter(asyncRoutes, routeFilter); + } + + this.setRouters(accessedRouters); + this.setMenus(accessedRouters); + return toRaw(accessedRouters); + }, + + setRouters(routers) { + this.addRouters = routers; + this.routers = constantRouter.concat(routers); + }, + setMenus(menus) { this.menus = menus; }, + setDynamicAddedRoute(added: boolean) { this.isDynamicAddedRoute = added; }, + }, +}); +``` + +## Store 间协作 + +登录流程中 Store 协作: + +```typescript +// 路由守卫中的调用(router-guards.ts) +const userStore = useUserStoreWidthOut(); +const asyncRouteStore = useAsyncRouteStoreWidthOut(); + +// 1. 获取用户信息(设置 permissions) +const userInfo = await userStore.GetInfo(); +// 2. 根据用户信息生成动态路由 +const routes = await asyncRouteStore.generateRoutes(userInfo); +// 3. 动态添加路由 +routes.forEach((item) => router.addRoute(item)); +``` + +## 使用方式 + +在组件中使用 store: + +```vue + +``` + +在 setup 外使用 store(需要传入 pinia 实例): + +```typescript +import { store } from '@/store'; +import { useUserStore } from '@/store/modules/user'; + +export function useUserStoreWidthOut() { + return useUserStore(store); +} +``` + +:::tip 温馨提示 +Token 通过 `Storage` 工具类管理,支持自动过期(默认 7 天)。退出登录时 `storage.clear()` 会清除所有 localStorage 数据。权限列表 `permissions` 在 `GetInfo` 时从后端获取,存储在 user store 中供 `v-perm` 指令使用。 +::: + +## 总结 + +Pinia 状态管理包含 user(登录与权限)、asyncRoute(动态路由与菜单)、projectSetting(项目配置)等核心模块。user store 管理 Token 和权限列表,asyncRoute store 管理动态路由生成和组件缓存。Store 之间通过路由守卫协调工作,完成登录 → 获取信息 → 生成路由 → 挂载应用的完整流程。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/style.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/style.md new file mode 100644 index 0000000..260bdfc --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/style.md @@ -0,0 +1,153 @@ +# 样式与主题 + +:::tip 说明 +前端样式方案采用 Tailwind CSS + SCSS + ElementPlus 主题覆盖,支持深色模式切换。全局 SCSS 变量通过 Vite 的 `additionalData` 注入。 +::: + +## 样式架构 + +``` +src/styles/ +├── var.scss # SCSS 全局变量 +├── index.scss # 入口文件,引入所有样式 +├── tailwindbase.css # Tailwind CSS 基础样式 +└── element-override.css # ElementPlus 主题覆盖(可选) +``` + +## SCSS 全局变量 + +Vite 配置中通过 `additionalData` 注入全局 SCSS 变量: + +```typescript +// vite.config.ts +css: { + preprocessorOptions: { + scss: { + additionalData: `@import "src/styles/var.scss";`, + }, + }, +}, +``` + +这意味着 `var.scss` 中定义的变量在所有 `.vue` 和 `.scss` 文件中自动可用,无需手动 `@import`。 + +## Tailwind CSS + +项目使用 Tailwind CSS 提供原子化样式能力,基础样式通过 `tailwindbase.css` 引入: + +```typescript +// main.ts +import './styles/tailwindbase.css'; +``` + +### 常用类名 + +```vue + +
padding 1rem, margin 0.5rem
+ + +
Flex 布局
+ + +

文字样式

+ + +
响应式宽度
+``` + +## ElementPlus 主题 + +ElementPlus 通过 CSS 变量支持主题定制: + +```typescript +// main.ts +import 'element-plus/dist/index.css'; +import 'element-plus/theme-chalk/display.css'; +import 'element-plus/theme-chalk/dark/css-vars.css'; // 深色模式 CSS 变量 +``` + +### ElementPlus 全局引入 + +```typescript +// src/plugins/setupElement.ts +import ElementPlus from 'element-plus'; +import zhCn from 'element-plus/es/locale/lang/zh-cn'; + +export function setupElement(app) { + app.use(ElementPlus, { locale: zhCn, size: 'default' }); +} +``` + +## 深色模式 + +通过 ElementPlus 的 `dark/css-vars.css` 和 Tailwind 的 `dark` 模式实现深色主题: + +```vue + + + +``` + +深色模式样式: + +```css +.dark { + --el-bg-color: #1d1e1f; + --el-text-color-primary: #e5eaf3; + --el-border-color: #4c4d4f; +} +``` + +## 设计配置 Store + +深色模式和主题色通过 `designSetting` store 管理: + +```typescript +// src/hooks/setting/useDesignSetting.ts + +export function useDesignSetting() { + return { + getAppTheme: computed(() => designStore.appTheme), + getDarkTheme: computed(() => designStore.darkTheme), + }; +} +``` + +## 页面样式约定 + +### PageWrapper + +页面使用 `PageWrapper` 组件包裹,提供统一的页面容器样式: + +```vue + +``` + +### proCard 样式 + +`proCard` 是项目定义的卡片样式类,用于统一卡片组件的间距和边框。 + +:::tip 温馨提示 +优先使用 Tailwind 原子类实现样式,避免编写自定义 CSS。需要全局覆盖时使用 CSS 变量。SCSS 变量通过 Vite `additionalData` 自动注入,无需每个文件手动引入。 +::: + +## 总结 + +样式方案采用 Tailwind CSS 提供原子化样式 + SCSS 全局变量(Vite `additionalData` 注入)+ ElementPlus 主题覆盖,支持深色模式切换。`designSetting` store 管理主题配置,`PageWrapper` 组件提供统一页面容器。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/table.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/table.md new file mode 100644 index 0000000..9776a97 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/table.md @@ -0,0 +1,266 @@ +# BasicTable 表格 + +:::tip 说明 +`BasicTable` 位于 `src/components/Table/src/Table.vue`,基于 ElementPlus 的 `el-table` 二次封装,提供列定义、数据请求、分页、操作列、多选、全屏、密度调整、列设置等能力。 +::: + +## 组件架构 + +``` +src/components/Table/ +├── src/ +│ ├── Table.vue # 主组件 +│ ├── props.ts # 属性定义 +│ ├── types/table.ts # 类型定义 +│ ├── hooks/ +│ │ ├── useLoading.ts # loading 状态 +│ │ ├── useColumns.ts # 列管理 +│ │ ├── useDataSource.ts # 数据源管理 +│ │ ├── usePagination.ts # 分页管理 +│ │ └── useTableContext.ts # 表格上下文 +│ └── components/ +│ ├── Render.tsx # 渲染函数组件 +│ └── settings/ +│ └── ColumnSetting.vue # 列设置面板 +├── index.ts # 导出 TableAction 等 +└── src/TableAction.vue # 操作列按钮组件 +``` + +## 基本用法 + +```vue + + + +``` + +## 列定义 + +`columns` 定义表格列,支持 `render` 函数自定义渲染: + +```typescript +// src/views/system/position/columns.ts + +import { h } from 'vue'; +import { ElTag } from 'element-plus'; + +export const columns = [ + { type: 'selection' }, + { label: 'ID', prop: 'id', fixed: 'left', width: 50 }, + { label: '岗位名称', prop: 'name', minWidth: 100 }, + { + label: '岗位状态', + prop: 'status', + minWidth: 100, + render(record) { + return h( + ElTag, + { type: record.row.status == 1 ? 'success' : 'danger' }, + { default: () => (record.row.status == 1 ? '正常' : '停用') }, + ); + }, + }, + { label: '排序', prop: 'sort', minWidth: 100 }, + { label: '创建人', prop: 'createUser', minWidth: 100 }, + { label: '创建时间', prop: 'createTime', width: 180 }, +]; +``` + +### 列配置项 + +| 属性 | 类型 | 说明 | +|------|------|------| +| `prop` | string | 字段名 | +| `label` | string | 列标题 | +| `width` | number | 固定宽度 | +| `minWidth` | number | 最小宽度 | +| `type` | string | 列类型:`selection` / `index` | +| `fixed` | string | 固定列:`left` / `right` | +| `render` | function | 自定义渲染函数,接收 `{ row, column, index }` | +| `isSlot` | boolean | 是否使用插槽渲染 | +| `ellipsis` | boolean | 是否溢出省略 | + +`render` 函数中 `record.row` 是行数据,`record.$index` 是行索引。 + +## 数据请求 + +`request` 属性传入数据请求函数,组件自动管理分页和 loading: + +```typescript +const loadDataTable = async (res: any) => { + const result = await getPositionList({ ...formParams, ...res }); + return result; // 返回 { data, count } 结构 +}; +``` + +内部通过 `useDataSource` Hook 实现:自动传入 `pageNo`、`pageSize` 参数,自动设置 `total`,自动管理 loading 状态。 + +## 操作列 + +`actionColumn` 使用 `TableAction` 组件渲染行操作按钮,支持权限控制: + +```typescript +import { TableAction } from '@/components/Table'; + +const actionColumn = reactive({ + width: 200, + label: '操作', + prop: 'action', + fixed: 'right', + render(record) { + return h(TableAction, { + style: 'button', + actions: [ + { + label: '编辑', + icon: 'Edit', + type: 'warning', + onClick: handleEdit.bind(null, record), + auth: ['sys:position:update'], // 权限节点 + }, + { + label: '删除', + icon: 'Delete', + type: 'danger', + onClick: handleDelete.bind(null, record), + auth: ['sys:position:delete'], // 权限节点 + }, + ], + }); + }, +}); +``` + +`TableAction` 的 `auth` 属性接受权限数组,内部通过 `usePermission` Hook 检查,无权限时自动隐藏按钮。 + +## 多选 + +通过 `selection-change` 事件获取选中行: + +```typescript +const selectionData = ref([]); + +function onSelectionChange(value) { + selectionData.value = value; +} + +// 批量删除 +async function handleDelete(record) { + let ids = []; + if (!record) { + ids = selectionData.value.map(({ id }) => id); + } + + await confirm('确定要删除?'); + record ? await positionDelete(record.row.id) : await positionBatchDelete(ids); + message('删除成功'); + reloadTable(); +} +``` + +## 刷新表格 + +通过 ref 调用 `reload` 或 `reloadTable` 方法: + +```typescript +const tableRef = ref(); + +// 刷新到第一页 +function reloadTable(noRefresh = '') { + tableRef.value.reload(noRefresh ? {} : { pageNo: 1 }); +} + +// 操作成功后刷新 +const handleAdd = async () => { + await positionAdd(data); + message('操作成功'); + reloadTable(); +}; +``` + +## 表格工具栏 + +BasicTable 内置右侧工具栏,支持: + +- 刷新按钮(redo) +- 密度切换(small / default / large) +- 列设置面板(ColumnSetting) +- 全屏切换 +- 斑马纹开关 + +通过 `showTableSetting` 和 `tableSetting` 属性控制: + +```vue + +``` + +## 搜索表单联动 + +列表页中 BasicForm 和 BasicTable 的典型联动模式: + +```typescript +// src/views/system/position/index.vue + +const formParams = reactive({ name: '', status: '' }); + +const [register, {}] = useForm({ + labelWidth: 80, + layout: 'horizontal', + colProps: { span: 6 } as ColProps, + submitOnReset: true, + schemas, // querySchemas.ts 中定义 +}); + +// 表单提交:更新查询参数,刷新表格 +function handleSubmit(values: Recordable) { + handleReset(); + for (const key in values) { formParams[key] = values[key]; } + reloadTable(); +} + +// 表单重置:清空查询参数,刷新表格 +function handleReset() { + for (const key in formParams) { formParams[key] = ''; } +} + +// 数据请求:合并表单参数和分页参数 +const loadDataTable = async (res: any) => { + const result = await getPositionList({ ...formParams, ...res }); + return result; +}; +``` + +## defineExpose + +组件通过 `defineExpose` 暴露的方法: + +| 方法 | 说明 | +|------|------| +| `reload(opt?)` | 刷新表格(可指定 pageNo) | +| `restReload()` | 重置后刷新 | +| `reloadTable(opt?)` | 刷新表格(清空选中行) | +| `clearSelection()` | 清空多选 | +| `getDataSource()` | 获取当前数据源 | +| `setTableData(data)` | 设置表格数据 | +| `setColumns(columns)` | 设置列 | +| `setLoading(loading)` | 设置 loading | + +:::tip 温馨提示 +BasicTable 内部自动处理分页参数(pageNo/pageSize),调用方只需提供请求函数。操作列的权限控制通过 `TableAction` 的 `auth` 属性实现,与后端 `@permission_required` 对应。`render` 函数使用 Vue 的 `h()` 函数创建虚拟 DOM。 +::: + +## 总结 + +BasicTable 封装了列定义(含 `render` 函数自定义渲染)、数据请求(自动分页和 loading)、操作列(`TableAction` + 权限控制)、多选、全屏、密度、列设置等表格常用能力。通过 `columns` 配置化定义列,`request` 函数自动管理数据加载,`actionColumn` 配置操作按钮。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/utils.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/utils.md new file mode 100644 index 0000000..3e64881 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/utils.md @@ -0,0 +1,241 @@ +# 工具函数 + +:::tip 说明 +前端工具函数位于 `src/utils/` 目录,提供认证管理、本地存储、日期格式化、树结构操作、文件下载等通用能力。 +::: + +## auth.ts + +认证与通用工具函数,位于 `src/utils/auth.ts`: + +```typescript +// 外部链接判断 +export function isExternal(path: string) { + return /^(https?:|mailto:|tel:)/.test(path); +} + +// 路径规范化 +export function getNormalPath(path: string) { + if (path.length === 0 || !path || path == 'undefined') return path; + const newPath = path.replace('//', '/'); + return newPath[newPath.length - 1] === '/' ? newPath.slice(0, -1) : newPath; +} + +// 确认弹窗封装 +export function confirm(msg: string, type: 'warning') { + return ElMessageBox.confirm(msg, '温馨提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: type, + }); +} + +// 消息提示封装 +export function message(msg: string, type = 'success') { + ElMessage[type](msg); +} + +// 文件流下载 +export function streamFileDownload(file: any, fileName = '文件名称.zip') { + const blob = new Blob([file], { type: 'application/octet-stream;charset=UTF-8' }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', fileName); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); +} +``` + +### 树结构操作 + +`auth.ts` 中还包含树结构转换工具: + +```typescript +// 数组转树 +export const arrayToTree = ( + data: any[], + props = { id: 'id', parentId: 'pid', children: 'children' }, +) => { + data = cloneDeep(data); + const { id, parentId, children } = props; + const result: any[] = []; + const map = new Map(); + data.forEach((item) => { + map.set(item[id], item); + const parent = map.get(item[parentId]); + if (parent) { + parent[children] = parent[children] ?? []; + parent[children].push(item); + } else { + result.push(item); + } + }); + return result; +}; + +// 树转数组(广度优先) +export const treeToArray = (data: any[], props = { children: 'children' }) => { + data = cloneDeep(data); + const newData = []; + const queue: any[] = []; + data.forEach((child) => queue.push(child)); + while (queue.length) { + const item = queue.shift(); + if (item[children]) { + item[children].forEach((child) => queue.push(child)); + delete item[children]; + } + newData.push(item); + } + return newData; +}; + +// 递归查找路径 +export const findTreeByPath = (tree, path, result = []) => { + for (const node of tree) { + if (node.path === path) result.push(node); + if (node.children?.length) findTreeByPath(node.children, path, result); + } + return result; +}; +``` + +## Storage.ts + +本地存储封装,支持过期时间和 cookie,位于 `src/utils/Storage.ts`: + +```typescript +const DEFAULT_CACHE_TIME = 7 * 24 * 60 * 60 * 1000; // 7 天 + +export const createStorage = ({ prefixKey = '', storage = localStorage } = {}) => { + const Storage = class { + private storage = storage; + private prefixKey?: string = prefixKey; + + private getKey(key: string) { + return `${this.prefixKey}${key}`.toUpperCase(); // 自动大写 + } + + // 设置缓存(支持过期时间) + set(key: string, value: any, expire: number | null = DEFAULT_CACHE_TIME) { + const stringData = JSON.stringify({ + value, + expire: expire !== null ? Date.now() + expire : null, + }); + this.storage.setItem(this.getKey(key), stringData); + } + + // 读取缓存(过期自动删除) + get(key: string, def: any = null) { + const item = this.storage.getItem(this.getKey(key)); + if (item) { + try { + const data = JSON.parse(item); + const { value, expire } = data; + if (expire === null || expire >= Date.now()) return value; + this.remove(key); // 过期删除 + } catch (e) { return def; } + } + return def; + } + + remove(key: string) { this.storage.removeItem(this.getKey(key)); } + clear(): void { this.storage.clear(); } + }; + return new Storage(); +}; + +export const storage = createStorage(); +``` + +Store mutation-types 定义了存储键名: + +```typescript +// src/store/mutation-types.ts +export const ACCESS_TOKEN = 'ACCESS-TOKEN'; // 用户 token +export const CURRENT_USER = 'CURRENT-USER'; // 当前用户信息 +export const IS_LOCKSCREEN = 'IS-LOCKSCREEN'; // 是否锁屏 +export const TABS_ROUTES = 'TABS-ROUTES'; // 标签页 +export const FIRST_ROUTE = 'FIRST-ROUTE'; // 菜单第一个路由 +``` + +## dateUtil.ts + +日期格式化工具,基于 dayjs,位于 `src/utils/dateUtil.ts`: + +```typescript +import dayjs from 'dayjs'; + +const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm'; +const DATE_FORMAT = 'YYYY-MM-DD '; + +export function formatToDateTime(date: Date, formatStr = DATE_TIME_FORMAT): string { + return dayjs(date).format(formatStr); +} + +export function formatToDate(date: Date, formatStr = DATE_FORMAT): string { + return dayjs(date).format(formatStr); +} +``` + +`dateUtil` 也被 `vite.config.ts` 使用,记录构建时间戳: + +```typescript +// vite.config.ts +import { formatToDateTime } from './src/utils/dateUtil'; +const __APP_INFO__ = { + pkg: { dependencies, devDependencies, name, version }, + lastBuildTime: formatToDateTime(new Date()), +}; +``` + +## httpEnum.ts + +HTTP 相关枚举定义,位于 `src/enums/httpEnum.ts`: + +```typescript +export enum ResultEnum { + SUCCESS = 0, // 业务成功 + ERROR = 1, // 业务失败 + TIMEOUT = 401, // 登录超时 +} + +export enum RequestEnum { + GET = 'GET', + POST = 'POST', + PUT = 'PUT', + DELETE = 'DELETE', +} + +export enum ContentTypeEnum { + JSON = 'application/json;charset=UTF-8', + FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8', + FORM_DATA = 'multipart/form-data;charset=UTF-8', +} +``` + +## pageEnum.ts + +页面路径枚举,位于 `src/enums/pageEnum.ts`: + +```typescript +export enum PageEnum { + BASE_LOGIN = '/login', + BASE_LOGIN_NAME = 'Login', + REDIRECT = '/redirect', + BASE_HOME = '/dashboard', + BASE_HOME_REDIRECT = '/dashboard/console', + ERROR_PAGE_NAME = 'ErrorPage', +} +``` + +:::tip 温馨提示 +`Storage` 工具类自动将键名转为大写(如 `access-token` → `ACCESS-TOKEN`),存储时包含过期时间,读取时自动检查是否过期。`auth.ts` 中的 `confirm` 和 `message` 函数是对 ElementPlus 的简封装,在业务组件中直接调用即可。 +::: + +## 总结 + +工具函数库提供了 Storage(本地存储,支持过期)、auth(认证管理、树结构操作、文件下载)、dateUtil(日期格式化)、httpEnum/pageEnum(枚举定义)等通用能力。`confirm` 和 `message` 是 ElementPlus 的简封装,`arrayToTree`/`treeToArray` 处理菜单数据转换。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/view.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/view.md new file mode 100644 index 0000000..fb6f90e --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/frontend/view.md @@ -0,0 +1,471 @@ +# 页面视图开发 + +:::tip 说明 +页面视图位于 `src/views/` 目录,每个业务模块遵循 `index.vue`(列表页)+ `edit.vue`(编辑弹窗)的标准模式,辅以 `columns.ts`(列定义)和 `querySchemas.ts`(搜索表单)配置文件。 +::: + +## 目录结构 + +``` +src/views/ +├── system/ +│ ├── position/ +│ │ ├── index.vue # 岗位列表页 +│ │ ├── edit.vue # 岗位编辑弹窗 +│ │ ├── columns.ts # 表格列定义 +│ │ └── querySchemas.ts # 搜索表单定义 +│ ├── user/ +│ │ ├── index.vue +│ │ └── edit.vue +│ ├── role/ +│ │ ├── index.vue +│ │ └── edit.vue +│ └── ... +├── cms/ +│ ├── article/ +│ └── ... +└── ... +``` + +## 文件职责 + +| 文件 | 职责 | 说明 | +|------|------|------| +| `index.vue` | 列表页 | 搜索表单 + 工具栏 + BasicTable + 编辑弹窗引用 | +| `edit.vue` | 编辑弹窗 | el-dialog + el-form,懒加载 | +| `columns.ts` | 表格列定义 | 定义列 prop、label、render 函数等 | +| `querySchemas.ts` | 搜索表单定义 | FormSchema 数组,定义搜索字段 | + +## index.vue 列表页 + +以岗位管理为例,完整结构如下: + +```vue + + + + + + +``` + +## edit.vue 编辑弹窗 + +```vue + + + + + +``` + +## 数据流 + +``` +页面加载 → BasicForm 注册 → BasicTable request 触发 → loadDataTable → API → 渲染表格 +用户搜索 → handleSubmit → 更新 formParams → reloadTable → 重新请求 +用户新增 → handleAdd → positionId=0 → editVisible=true → edit.vue 加载 → 表单填写 → 提交 +用户编辑 → handleEdit → positionId=id → editVisible=true → edit.vue 加载详情 → 修改 → 提交 +用户删除 → confirm → API → reloadTable +``` + +## columns.ts + +```typescript +// src/views/system/position/columns.ts +import { h } from 'vue'; +import { ElTag } from 'element-plus'; + +export const columns = [ + { + type: 'selection', + }, + { + label: 'ID', + prop: 'id', + fixed: 'left', + width: 50, + }, + { + label: '岗位名称', + prop: 'name', + minWidth: 100, + }, + { + label: '岗位状态', + prop: 'status', + minWidth: 100, + render(record) { + return h( + ElTag, + { + type: record.row.status == 1 ? 'success' : 'danger', + }, + { + default: () => (record.row.status == 1 ? '正常' : '停用'), + }, + ); + }, + }, + { + label: '排序', + prop: 'sort', + minWidth: 100, + }, + { + label: '创建人', + prop: 'createUser', + minWidth: 100, + }, + { + label: '创建时间', + prop: 'createTime', + width: 180, + }, +]; +``` + +## querySchemas.ts + +```typescript +// src/views/system/position/querySchemas.ts +import { FormSchema } from '@/components/Form/index'; +export const schemas: FormSchema[] = [ + { + field: 'name', + component: 'Input', + label: '岗位名称', + componentProps: { + placeholder: '请输入岗位名称', + }, + }, + { + field: 'status', + component: 'Select', + label: '状态', + componentProps: { + placeholder: '请选择状态', + clearable: true, + options: [ + { + label: '正常', + value: '1', + }, + { + label: '禁用', + value: '2', + }, + ], + }, + }, +]; +``` + +:::tip 温馨提示 +`columns.ts` 和 `querySchemas.ts` 独立为配置文件,便于维护和复用。`columns` 中的 `render` 函数使用 Vue 的 `h()` 创建虚拟 DOM,可实现任意复杂的自定义渲染。`querySchemas` 中的 `options` 硬编码或通过字典 API 动态获取。 +::: + +## 总结 + +页面视图遵循 `index.vue` + `edit.vue` + `columns.ts` + `querySchemas.ts` 标准模式。列表页使用 `BasicForm` 搜索 + `BasicTable` 展示数据 + `TableAction` 操作按钮,编辑弹窗使用 `defineAsyncComponent` 懒加载 + `useLockFn` 防重复提交。数据流清晰:搜索更新参数 → 刷新表格,操作成功 → emit 事件 → 刷新表格。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/integration/api-contract.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/integration/api-contract.md new file mode 100644 index 0000000..1592cd7 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/integration/api-contract.md @@ -0,0 +1,168 @@ +# 接口响应规范 + +:::tip 说明 +后端所有接口统一返回 `R.ok()` / `R.failed()` 格式的 JSON 响应,前端通过 `VAxios` 的 `transformRequestData` 拦截器统一解包处理。 +::: + +## 响应格式 + +### 成功响应 + +```json +{ + "code": 0, + "data": { "id": 1, "name": "测试岗位", "status": 1 }, + "msg": "操作成功", + "ok": true +} +``` + +### 失败响应 + +```json +{ + "code": 1, + "data": null, + "msg": "岗位名称不能重复", + "ok": false +} +``` + +### 分页响应 + +```json +{ + "code": 0, + "data": [ + { "id": 1, "name": "岗位1", "status": 1, "status_name": "正常" }, + { "id": 2, "name": "岗位2", "status": 2, "status_name": "停用" } + ], + "count": 100, + "msg": "操作成功", + "ok": true +} +``` + +## 后端实现 + +位于 `src/core/response.py`: + +```python +def ok(self, data=None, msg="操作成功", code=0, **kwargs): + """生成成功响应""" + body = {"code": code, "data": data, "msg": msg, "ok": True} + if kwargs: + body.update(kwargs) + self._api_result = body + self.finish(body) + +def failed(self, msg="操作失败", code=1, data=None, **kwargs): + """生成失败响应""" + body = {"code": code, "data": data, "msg": msg, "ok": False} + if kwargs: + body.update(kwargs) + self._api_result = body + self.finish(body) +``` + +### 使用示例 + +```python +# 成功 +return R.ok(data=user_info) +return R.ok(data=list, count=total) # 分页 +return R.ok(msg="添加成功") + +# 失败 +return R.failed(msg="用户名已存在") +return R.failed("参数错误") +``` + +## 前端拦截器 + +`VAxios` 的 `transformRequestData` 统一解包响应: + +```typescript +// src/utils/http/axios/index.ts +transformRequestData: (res: AxiosResponse, options: RequestOptions) => { + const { code, data, msg } = res.data; + + // 业务成功(code === 0):直接返回 data + if (code === ResultEnum.SUCCESS) { + return data; + } + + // 业务失败(code === 1):弹出错误提示 + if (code === ResultEnum.ERROR) { + ElMessage.error(msg); + throw new Error(msg); + } + + // 登录超时(code === 401):弹出确认框,跳转登录页 + if (code === ResultEnum.TIMEOUT) { + ElMessageBox.confirm('登录身份已失效,请重新登录!', '提示', { + confirmButtonText: '确定', + showCancelButton: false, + type: 'warning', + }).then(() => { + storage.clear(); + window.location.href = '/login'; + }); + throw new Error(msg); + } +}, +``` + +解包后调用方拿到的是 `data` 字段内容: + +```typescript +// 普通接口:data 就是业务数据 +const result = await positionAdd(formData); +// result = { id: 1, name: "测试岗位", ... } + +// 分页接口:data 包含 list 和 count +const result = await getPositionList(params); +// result = { data: [...], count: 100, msg: "操作成功", ok: true, code: 0 } +``` + +## 字段说明 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `code` | number | 业务码:0=成功,1=失败,401=登录超时 | +| `data` | any | 业务数据 | +| `count` | number | 分页总数(仅分页接口) | +| `msg` | string | 提示消息 | +| `ok` | boolean | 业务是否成功 | + +## 错误码规范 + +| code | 说明 | 前端行为 | +|------|------|----------| +| 0 | 成功 | 返回 `data` | +| 1 | 业务失败 | `ElMessage.error(msg)` + `throw` | +| 401 | 未登录或 Token 过期 | 弹出确认框 → 跳转登录页 | + +HTTP 状态码始终为 200,业务成功/失败通过 `code` 字段区分。 + +## 前端错误展示 + +业务错误通过 `ElMessage.error` 弹出提示,无需组件手动处理: + +```typescript +// 组件中直接调用,错误由拦截器处理 +try { + await positionAdd(formData); + message('操作成功'); +} catch (e) { + // 拦截器已弹出 ElMessage.error,此处可选处理 +} +``` + +:::tip 温馨提示 +HTTP 状态码始终为 200,业务成功/失败通过 `code` 字段区分。这避免了 HTTP 状态码与业务状态码的混淆。前端拦截器统一处理错误提示,业务代码只需处理成功逻辑。 +::: + +## 总结 + +接口响应规范统一使用 `R.ok()`/`R.failed()` 格式,`code=0` 表示成功返回 `data`,`code=1` 表示失败弹出 `msg`,`code=401` 表示登录超时跳转登录页。前端 `VAxios` 的 `transformRequestData` 拦截器统一解包,调用方直接使用业务数据。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/integration/dict-sync.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/integration/dict-sync.md new file mode 100644 index 0000000..ab60690 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/integration/dict-sync.md @@ -0,0 +1,365 @@ +# 字典数据联调 + +:::tip 说明 +字典数据通过后端字典 API 获取,前端在下拉选择器中渲染选项。字典项的 `value` 字段统一为字符串类型,前端使用时需要注意类型转换。 +::: + +## 后端字典 API + +### 获取字典数据 + +``` +GET /api/v1/dict/data/{code} +``` + +返回: + +```json +{ + "code": 0, + "data": [ + { "id": 1, "label": "系统", "value": "0" }, + { "id": 2, "label": "业务", "value": "1" } + ], + "msg": "操作成功", + "ok": true +} +``` + +### 字典数据结构 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | number | 字典项 ID | +| `label` | string | 显示文本 | +| `value` | string | 值(注意:统一为字符串) | + +:::warning 值类型注意 +字典项的 `value` 字段统一为字符串类型,即使存储的是数字。前端使用时需要注意类型转换。 +::: + +--- + +## 序列化映射 + +BaseService 的 `serialize_maps` 自动将字典值映射为显示名,列表和详情接口均生效。 + +### 基本用法(以 param 模块为例) + +```python +# src/modules/param/service.py +class ParamService(BaseService(Param]): + repo = param_repo + model = Param + # 字段名 → 数据字典编码 + serialize_maps = {'type': 'param_type', 'status': 'param_status'} +``` + +`serialize_maps` 的 key 是模型字段名,value 是数据字典编码。BaseService 序列化时自动: + +1. 取出模型字段的原始值(如 `type=0`) +2. 从数据字典查询对应的 label(如 `param_type` 字典中 `value='0'` 的 `label='系统'`) +3. 以 `字段名驼峰 + Text` 为 key 写入响应(如 `type` → `typeText`) + +### 响应示例 + +模型原始数据: + +``` +Param(id=1, name="默认密码", code="DEFAULT_PASSWORD", value="123456", type=0, status=1, sort=1) +``` + +列表/详情接口返回: + +```json +{ + "id": 1, + "name": "默认密码", + "code": "DEFAULT_PASSWORD", + "value": "123456", + "type": 0, + "typeText": "系统", + "status": 1, + "statusText": "正常", + "sort": 1, + "note": null, + "createUser": "管理员", + "createTime": "2025-03-06 14:30:25" +} +``` + +### 字段命名规则 + +| 模型字段 | serialize_maps key | 响应字段 | 说明 | +|----------|-------------------|----------|------| +| `type` | `'type'` | `typeText` | snake_case → camelCase + `Text` | +| `status` | `'status'` | `statusText` | 同上 | +| `log_type` | `'log_type'` | `logTypeText` | 下划线也转驼峰 | +| `source` | `'source'` | `sourceText` | 同上 | + +转换逻辑(`src/utils/string.py`): + +```python +def convert_camel_case(name: str) -> str: + """snake_case → camelCase(log_type → logType)""" + parts = name.split('_') + return parts[0] + ''.join(word.capitalize() for word in parts[1:]) +``` + +### 数据字典编码 + +`serialize_maps` 的 value 必须是已配置的数据字典编码。常用编码: + +| 字典编码 | 说明 | 字典项示例 | +|----------|------|-----------| +| `param_type` | 参数类型 | 0-系统, 1-业务 | +| `param_status` | 参数状态 | 1-正常, 2-禁用 | +| `operation_type` | 操作类型 | 0-其他, 1-新增, 2-修改, 3-删除 | +| `operation_source` | 操作来源 | 0-后台, 1-前台 | +| `operation_status` | 操作状态 | 0-正常, 1-异常 | +| `log_type` | 日志类型 | 1-登录, 2-退出 | +| `login_source` | 登录来源 | 0-后台, 1-前台 | +| `login_status` | 登录状态 | 0-成功, 1-失败 | + +字典编码在后台「字典管理」中配置,或通过 `scripts/seed_dict.py` 初始化。 + +### 完整示例:param 模块 + +**后端 Service**: + +```python +# src/modules/param/service.py +class ParamService(BaseService(Param]): + repo = param_repo + model = Param + page_like_fields = ('name', 'code') + page_eq_fields = ('type', 'status') + page_order_by = (('sort', 'asc'), ('id', 'desc')) + unique_fields = {'name': '参数名称不能重复', 'code': '参数编码不能重复'} + # 枚举显示名:字段名 → 数据字典编码 + serialize_maps = {'type': 'param_type', 'status': 'param_status'} +``` + +**前端 columns.ts**: + +```typescript +// src/views/system/param/columns.ts +{ + label: '参数类型', + prop: 'type', + width: 100, + // 方式一:直接使用后端返回的 typeText 字段 + render(record) { + return h(ElTag, + { type: record.row.type == 0 ? 'primary' : 'warning' }, + { default: () => record.row.typeText }); + }, +}, +{ + label: '状态', + prop: 'status', + width: 100, + // 方式二:直接使用后端返回的 statusText 字段 + render(record) { + return h(ElTag, + { type: record.row.status == 1 ? 'success' : 'danger' }, + { default: () => record.row.statusText }); + }, +}, +``` + +### 其他模块示例 + +```python +# src/modules/operation_log/service.py +serialize_maps = { + 'type': 'operation_type', # typeText: "新增"/"修改"/"删除" + 'source': 'operation_source', # sourceText: "后台"/"前台" + 'status': 'operation_status', # statusText: "正常"/"异常" +} + +# src/modules/login_log/service.py +serialize_maps = { + 'log_type': 'log_type', # logTypeText: "登录"/"退出" + 'source': 'login_source', # sourceText: "后台"/"前台" + 'status': 'login_status', # statusText: "成功"/"失败" +} +``` + +--- + +## 前端字典管理 + +### 搜索表单中的字典选项 + +搜索表单中直接硬编码选项(简单场景)或通过 API 获取(动态场景): + +**硬编码选项(简单场景)** + +```typescript +// src/views/system/param/querySchemas.ts +export const schemas: FormSchema[] = [ + { + field: 'type', + component: 'Select', + label: '参数类型', + componentProps: { + placeholder: '请选择参数类型', + clearable: true, + options: [ + { label: '系统', value: 0 }, + { label: '业务', value: 1 }, + ], + }, + }, + { + field: 'status', + component: 'Select', + label: '状态', + componentProps: { + placeholder: '请选择状态', + clearable: true, + options: [ + { label: '正常', value: 1 }, + { label: '禁用', value: 2 }, + ], + }, + }, +]; +``` + +**API 获取选项(动态场景)** + +```typescript +import { getDictItemByCode } from '@/api/common'; + +const typeOptions = ref([]); +const statusOptions = ref([]); + +onMounted(async () => { + const res1 = await getDictItemByCode('param_type'); + typeOptions.value = res1.data; // [{ label: '系统', value: '0' }, ...] + + const res2 = await getDictItemByCode('param_status'); + statusOptions.value = res2.data; +}); + +const schemas: FormSchema[] = [ + { + field: 'type', + component: 'Select', + label: '参数类型', + componentProps: { options: typeOptions }, + }, + { + field: 'status', + component: 'Select', + label: '状态', + componentProps: { options: statusOptions }, + }, +]; +``` + +### 前端字典 API + +字典相关 API 位于 `src/api/data/dictionary.ts`: + +```typescript +// src/api/data/dictionary.ts +import { http } from '@/utils/http/axios'; + +// 字典列表(分页) +export function getDictList(params?) { + return http.request({ url: '/dict/page', method: 'GET', params }); +} + +// 字典项列表(分页) +export function getDictItemList(params?) { + return http.request({ url: '/dict/item/page', method: 'GET', params }); +} +``` + +按字典编码获取字典项(下拉框用)位于 `src/api/common/index.ts`: + +```typescript +// src/api/common/index.ts +export function getDictItemByCode(code) { + return http.request({ + url: '/dict/item/getDictItemList/' + code, + method: 'GET', + }); +} +``` + +后端对应接口:`GET /api/v1/dict/data/{code}`(`src/modules/dict.py`)。 + +--- + +## 值类型处理 + +字典 value 统一为字符串,但业务字段可能为数字,需要注意转换: + +```typescript +// 字典选项值为字符串 +const options = [ + { label: '系统', value: '0' }, + { label: '业务', value: '1' }, +]; + +// 业务字段为数字 +formData.type = 0; + +// el-option 绑定时需要转换 + +``` + +搜索表单中的 `options` 的 `value` 为字符串 `'0'`/`'1'`,后端 `page_eq_fields` 中的 `type` 字段期望数字。前端 API 传参时自动转换。 + +--- + +## 字典管理 + +### 配置字典 + +在后台「字典管理」中配置字典类型和字典项: + +``` +字典类型: param_type(参数类型) +字典项: + - label: 系统, value: 0 + - label: 业务, value: 1 + +字典类型: param_status(参数状态) +字典项: + - label: 正常, value: 1 + - label: 禁用, value: 2 +``` + +### 字典类型命名规范 + +``` +sys_ —— 系统类字典(sys_status, sys_gender 等) +param_ —— 参数类字典(param_type, param_status 等) +operation_ —— 操作日志类字典(operation_type, operation_status 等) +login_ —— 登录日志类字典(log_type, login_source, login_status 等) +cms_ —— 内容类字典(cms_article_status 等) +biz_ —— 业务类字典 +``` + +--- + +## 联调流程 + +``` +1. 后台「字典管理」中新增字典类型和字典项 +2. 后端 service 设置 serialize_maps(字段名 → 字典编码) +3. 前端 columns.ts 使用 render 函数渲染状态标签(直接用 xxxText 字段) +4. 前端 querySchemas.ts 中配置搜索选项(硬编码或 API 获取) +5. 表单提交时 value 为字符串,后端存储时注意类型处理 +``` + +--- + +## 总结 + +字典数据联调通过后端字典 API + `serialize_maps` 映射显示名 + 前端 `render` 函数渲染标签实现。`serialize_maps` 的 value 为数据字典编码,BaseService 自动查询字典并以 `xxxText` 格式输出。字典值统一为字符串,前端使用时注意类型转换。简单场景硬编码选项,动态场景通过 API 获取。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/integration/perm-sync.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/integration/perm-sync.md new file mode 100644 index 0000000..6e47f14 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/integration/perm-sync.md @@ -0,0 +1,189 @@ +# 权限节点联动 + +:::tip 说明 +前后端权限通过统一的权限节点字符串联动,后端 `@permission_required` 校验权限,前端 `v-perm` 指令和 `TableAction` 的 `auth` 属性控制按钮显示。 +::: + +## 权限节点格式 + +统一格式:`sys:{module}:{action}` + +``` +sys:position:page —— 分页查询 +sys:position:detail —— 查询详情 +sys:position:list —— 获取列表 +sys:position:add —— 新增 +sys:position:update —— 编辑 +sys:position:delete —— 删除 +sys:position:batchDelete —— 批量删除 +sys:position:status —— 设置状态 +sys:position:export —— 导出 +sys:position:import —— 导入 +``` + +## 后端权限校验 + +Endpoint 使用 `@permission_required` 装饰器: + +```python +# src/modules/position/handlers.py +class PositionAddHandler(BaseHandler): + @permission_required("sys:position:add") + @check_demo + async def post(self): + return await position_service.add(self) +``` + +校验流程: + +``` +1. login_required 从 JWT Token 解析用户 ID +2. permission_required 查询用户角色 → 角色菜单 → 权限节点列表 +3. 检查 endpoint 要求的权限节点是否在列表中 +4. 用户 ID=1(管理员)跳过检查 +``` + +## 前端权限控制 + +### v-perm 指令 + +按钮使用 `v-perm` 指令(满足任一权限即可显示): + +```vue + + + 添加岗位 + + + 删除 + +``` + +### TableAction auth 属性 + +操作列通过 `TableAction` 组件的 `auth` 属性控制: + +```typescript +const actionColumn = { + render(record) { + return h(TableAction, { + style: 'button', + actions: [ + { + label: '编辑', + icon: 'Edit', + type: 'warning', + onClick: handleEdit.bind(null, record), + auth: ['sys:position:update'], + }, + { + label: '删除', + icon: 'Delete', + type: 'danger', + onClick: handleDelete.bind(null, record), + auth: ['sys:position:delete'], + }, + ], + }); + }, +}; +``` + +### usePermission Hook + +在逻辑中检查权限: + +```typescript +import { usePermission } from '@/hooks/web/usePermission'; + +const { hasPermission, hasSomePermission } = usePermission(); + +// v-if 条件渲染 +const showExportBtn = computed(() => hasPermission(['sys:position:export'])); +``` + +## 菜单配置 + +权限节点在菜单管理中配置,菜单类型为「按钮」: + +``` +菜单管理 → 系统管理 → 岗位管理 + ├── 岗位查询(sys:position:page)—— 类型:菜单 + ├── 岗位新增(sys:position:add)—— 类型:按钮 + ├── 岗位编辑(sys:position:update)—— 类型:按钮 + ├── 岗位删除(sys:position:delete)—— 类型:按钮 + ├── 岗位批量删除(sys:position:batchDelete)—— 类型:按钮 + └── 岗位状态(sys:position:status)—— 类型:按钮 +``` + +## 权限数据流 + +``` +后端菜单表 → adminMenus API → routerGenerator 生成路由表(含 permission 字段) + ↓ + userStore.GetInfo() → 获取 permissions 列表 + ↓ + v-perm 指令 mounted → hasSomePermission(permissions) + ↓ + 有权限 → 保留元素 / 无权限 → 移除元素 +``` + +前端权限列表在登录后通过 `getUserInfo` 接口获取,存储在 user store 的 `permissions` 数组中: + +```typescript +// src/store/modules/user.ts +GetInfo() { + return new Promise((resolve, reject) => { + getUserInfo().then((res) => { + if (res.permissions && res.permissions.length) { + this.setPermissions(res.permissions); + // permissions 示例:["sys:position:page", "sys:position:add", ...] + } + this.setUserInfo(res); + resolve(res); + }).catch(reject); + }); +}, +``` + +## 联调要点 + +``` +1. 后端 endpoint 的 permission_required 权限字符串必须与菜单配置的节点一致 +2. 前端 v-perm 指令和 TableAction auth 的权限字符串必须与菜单配置的节点一致 +3. 权限字符串区分大小写 +4. 用户 ID=1(管理员)拥有所有权限(后端跳过检查) +5. 前端权限列表包含 "*:*:*" 通配符时跳过所有检查 +``` + +## 常见问题 + +### 按钮不显示 + +``` +检查步骤: +1. 确认菜单管理中已配置对应的按钮权限节点 +2. 确认角色已分配该菜单权限 +3. 确认用户已关联该角色 +4. 确认前端 v-perm 字符串与菜单节点完全一致(区分大小写) +5. 确认 userStore.permissions 已正确加载 +``` + +### 接口 403 + +``` +检查步骤: +1. 确认后端 permission_required 字符串与菜单节点一致 +2. 确认菜单类型为「按钮」而非「菜单」 +3. 确认角色已分配该权限 +4. 确认 Token 未过期 +``` + +:::tip 温馨提示 +权限节点的命名建议遵循 `sys:{module}:{action}` 规范,保持前后端一致。新增模块时,先在菜单管理中配置权限节点,再在后端和前端使用。前端 `v-perm` 仅做 UI 层隐藏,真正的权限校验在后端。 +::: + +## 总结 + +前后端权限通过统一的权限节点字符串联动。后端 `@permission_required` 做真正的权限校验(用户 ID=1 跳过),前端 `v-perm` 指令和 `TableAction` 的 `auth` 属性做 UI 层隐藏。权限列表在登录时从后端获取,存储在 user store 中。`*:*:*` 通配符表示超级管理员。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/integration/proxy.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/integration/proxy.md new file mode 100644 index 0000000..14d77ba --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/integration/proxy.md @@ -0,0 +1,194 @@ +# 代理配置 + +:::tip 说明 +开发环境通过 Vite 代理转发 API 请求到后端,生产环境通过 Nginx 代理。代理配置由 `VITE_PROXY` 环境变量控制。 +::: + +## 开发环境代理 + +### Vite 配置 + +`vite.config.ts` 中通过 `createProxy` 工具函数解析 `VITE_PROXY` 环境变量生成代理配置: + +```typescript +// vite.config.ts +import { createProxy } from './build/vite/proxy'; + +export default ({ command, mode }: ConfigEnv): UserConfig => { + let VITE_PROXY: any = viteEnv.VITE_PROXY; + if (!VITE_PROXY) VITE_PROXY = '[[]]'; + try { + if (typeof VITE_PROXY === 'string') VITE_PROXY = JSON.parse(VITE_PROXY); + } catch (e) { + VITE_PROXY = [[]]; + } + + return { + server: { + host: true, + port: VITE_PORT, + proxy: createProxy(VITE_PROXY), + }, + }; +}; +``` + +### 环境变量配置 + +`.env` 文件中配置代理规则(JSON 数组格式): + +```bash +# 格式:[["前缀","目标地址"]] +VITE_PROXY=[["/api","http://127.0.0.1:8041/api/v1"]] +``` + +`createProxy` 函数将此配置转换为 Vite 的 `server.proxy` 格式,自动添加 `changeOrigin: true` 等选项。 + +### 代理规则 + +``` +前端请求: http://localhost:8001/api/v1/position/page +代理转发: http://127.0.0.1:8041/api/v1/position/page +``` + +`/api` 前缀的请求被代理到后端 `http://127.0.0.1:8041`,`changeOrigin: true` 修改请求头的 Host 为目标地址。 + +### 开发流程 + +``` +1. 启动后端: python app.py (127.0.0.1:8041) +2. 启动前端: pnpm dev (localhost:8001) +3. 浏览器访问 localhost:8001 +4. API 请求自动代理到 127.0.0.1:8041 +``` + +### 前端请求路径 + +前端 API 函数中的 URL 不含 `/api/v1` 前缀: + +```typescript +// src/api/system/position.ts +export function getPositionList(params?) { + return http.request({ + url: '/position/page', // 实际请求:/api/v1/position/page + method: 'GET', + params, + }); +} +``` + +`VAxios` 创建时配置了 `prefixUrl` 和 `urlPrefix`,`beforeRequestHook` 自动拼接: + +```typescript +// src/utils/http/axios/index.ts +const urlPrefix = globSetting.urlPrefix || '/v1'; + +createAxios({ + prefixUrl: urlPrefix, // '/v1' + requestOptions: { + joinPrefix: true, + apiUrl: globSetting.apiUrl, // '/api' + urlPrefix: urlPrefix, // '/v1' + }, +}); +``` + +最终请求路径:`/api` + `/v1` + `/position/page` = `/api/v1/position/page` + +## 生产环境代理 + +### Nginx 配置 + +```nginx +server { + listen 80; + server_name your-domain.com; + + # 前端静态文件 + location / { + root /path/to/dist; + index index.html; + try_files $uri $uri/ /index.html; # SPA 路由回退 + } + + # API 代理 + location /api/ { + proxy_pass http://127.0.0.1:8041/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 超时配置 + proxy_connect_timeout 30s; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + } +} +``` + +### Nginx 代理说明 + +| 配置项 | 说明 | +|--------|------| +| `proxy_pass` | 转发目标地址 | +| `proxy_set_header Host` | 传递原始 Host | +| `proxy_set_header X-Real-IP` | 传递客户端真实 IP | +| `proxy_set_header X-Forwarded-For` | 传递代理链 IP | +| `proxy_set_header X-Forwarded-Proto` | 传递原始协议 | +| `proxy_connect_timeout` | 连接超时 | +| `proxy_read_timeout` | 读取超时 | + +## 文件访问代理 + +上传文件的访问也需要代理: + +```nginx +# 文件访问 +location /api/file/ { + proxy_pass http://127.0.0.1:8041/api/file/; + proxy_set_header Host $host; + + # 静态文件缓存 + expires 7d; + add_header Cache-Control "public, immutable"; +} +``` + +:::tip 温馨提示 +后端的文件访问地址需要与前端一致。开发环境为 `http://127.0.0.1:8041/api/file`,生产环境需要在 `.env.production` 中配置 `VITE_GLOB_FILE_URL` 为实际域名。 +::: + +## Docker 代理 + +Docker 环境中通过 docker-compose 网络通信: + +```yaml +services: + nginx: + image: nginx:alpine + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/conf.d/default.conf + - ./dist:/usr/share/nginx/html + depends_on: + - backend + + backend: + build: ./backend + expose: + - "8041" +``` + +Nginx 配置中 `proxy_pass` 指向 Docker 服务名: + +```nginx +location /api/ { + proxy_pass http://backend:8041/api/; +} +``` + +## 总结 + +开发环境通过 `VITE_PROXY` 环境变量配置 Vite 代理,`createProxy` 函数解析 JSON 格式的代理规则。前端 API 路径不含 `/api/v1` 前缀,由 `VAxios` 的 `beforeRequestHook` 自动拼接。生产环境通过 Nginx 代理并配置超时、缓存等参数,Docker 环境通过 docker-compose 网络实现服务间通信。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/integration/upload-sync.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/integration/upload-sync.md new file mode 100644 index 0000000..0d11c7f --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/integration/upload-sync.md @@ -0,0 +1,193 @@ +# 文件上传联调 + +:::tip 说明 +文件上传涉及前端 Upload 组件、后端上传接口和 URL 拼接三个环节,需要前后端协同配置。 +::: + +## 上传流程 + +``` +1. 前端 Upload 组件选择文件 +2. VAxios.uploadFile() 发送 multipart/form-data POST 请求到 /api/v1/upload/uploadFile +3. 后端校验文件(扩展名、大小、魔术字节) +4. 后端保存文件到临时目录,返回文件信息(fileUrl 等) +5. 前端将 fileUrl 存入业务字段 +6. 业务数据提交时携带 fileUrl +7. 后端 save_file() 将临时文件迁移到正式目录 +``` + +## 前端上传实现 + +### VAxios.uploadFile + +`VAxios` 类内置 `uploadFile` 方法: + +```typescript +// src/utils/http/axios/Axios.ts +uploadFile(config: AxiosRequestConfig, params: UploadFileParams) { + const formData = new window.FormData(); + const customFilename = params.name || 'file'; + + if (params.filename) { + formData.append(customFilename, params.file, params.filename); + } else { + formData.append(customFilename, params.file); + } + + if (params.data) { + Object.keys(params.data).forEach((key) => { + const value = params.data![key]; + if (Array.isArray(value)) { + value.forEach((item) => formData.append(`${key}[]`, item)); + return; + } + formData.append(key, params.data![key]); + }); + } + + return this.axiosInstance.request({ + method: 'POST', + data: formData, + headers: { + 'Content-type': ContentTypeEnum.FORM_DATA, + ignoreCancelToken: true, + }, + ...config, + }); +} +``` + +### Upload 组件使用 + +```vue + + + +``` + +Upload 组件内部调用上传接口: + +```typescript +const handleUpload = async (file) => { + const formData = new FormData(); + formData.append('file', file); + const res = await uploadFile(formData); + // res.data.fileUrl = "http://127.0.0.1:8041/api/file/20250306/xxx.jpg" + emit('update:modelValue', res.data.fileUrl); +}; +``` + +## 后端上传接口 + +``` +POST /api/v1/upload/uploadFile +Content-Type: multipart/form-data +参数: file(文件字段) +``` + +返回: + +```json +{ + "code": 0, + "data": { + "originalName": "photo.jpg", + "fileExtension": "jpg", + "fileType": "image/jpeg", + "fileSize": 306995, + "fileName": "2025030614302512345.jpg", + "filePath": "/20250306/2025030614302512345.jpg", + "fileUrl": "http://127.0.0.1:8041/api/file/20250306/2025030614302512345.jpg" + }, + "msg": "上传成功", + "ok": true +} +``` + +## URL 拼接 + +### 后端拼接 + +后端 `save_file()` 函数处理文件迁移后返回相对路径,`get_file_url()` 拼接完整 URL: + +```python +def get_file_url(path): + if not path: + return "" + if path.find(TORNADO_FILE_URL) != -1: + return path + return TORNADO_FILE_URL + path +``` + +### 前端显示 + +前端显示图片时,需要拼接完整的 URL: + +```typescript +const getFileUrl = (path) => { + if (!path) return ''; + if (path.startsWith('http')) return path; + return import.meta.env.VITE_GLOB_FILE_URL + path; +}; +``` + +## 临时文件迁移 + +业务数据提交时,后端 service 调用 `save_file()` 将临时文件迁移到正式目录: + +```python +# 临时路径: {temp}/{日期}/{文件名} +# 正式路径: {upload_dir}/{分类}/{日期}/{文件名} + +def save_file(url, directory): + disk_path = _url_to_disk_path(url) + if disk_path.startswith(TORNADO_TEMP_PATH): + target_dir = os.path.join(TORNADO_UPLOAD_DIR, directory, time.strftime('%Y%m%d')) + new_path = os.path.join(target_dir, os.path.basename(disk_path)) + shutil.move(disk_path, new_path) + return _disk_to_url_path(new_path) + return _disk_to_url_path(disk_path) +``` + +## 配置一致性 + +确保以下配置一致: + +| 配置项 | 位置 | 说明 | +|--------|------|------| +| `TORNADO_FILE_URL` | 后端 `.env` | 文件访问域名 | +| `VITE_GLOB_FILE_URL` | 前端 `.env` | 文件访问域名 | +| Nginx `location` | Nginx 配置 | 文件代理路径 | + +:::tip 温馨提示 +开发环境和生产环境的文件访问域名不同,需要分别配置。`VITE_GLOB_FILE_URL` 通过 `useGlobSetting()` 获取,生产环境通常使用独立的文件服务器或 CDN。 +::: + +## 常见问题 + +### 图片不显示 + +``` +1. 检查 VITE_GLOB_FILE_URL 配置是否正确 +2. 检查 Nginx 是否配置了 /api/file/ 代理 +3. 检查文件是否已从临时目录迁移到正式目录 +``` + +### 上传失败 + +``` +1. 检查文件扩展名是否在白名单中 +2. 检查文件大小是否超过 UPLOAD_MAX_SIZE_MB 限制 +3. 检查后端服务是否正常运行 +``` + +## 总结 + +文件上传通过 `VAxios.uploadFile()` 发送 `multipart/form-data` 请求,后端保存到临时目录并返回 `fileUrl`。业务提交时后端 `save_file()` 迁移到正式目录。前端通过 `VITE_GLOB_FILE_URL` 拼接完整 URL 显示文件。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/context.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/context.md new file mode 100644 index 0000000..3270e80 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/context.md @@ -0,0 +1,147 @@ +# 请求上下文(ContextVar) + +:::tip 说明 +`src/core/context.py` 使用 Python 标准库 `contextvars` 管理请求级别的上下文信息,包括当前用户 ID、用户名、姓名和链路追踪 ID。这些变量在整个请求处理链路中自动传递,无需手动传参。 +::: + +## 核心变量 + +```python +# src/core/context.py +from contextvars import ContextVar + +# 当前登录用户 ID +current_user_id: ContextVar[int] = ContextVar("current_user_id", default=0) +# 当前登录用户名 +current_username: ContextVar[str] = ContextVar("current_username", default="") +# 当前登录用户姓名 +current_realname: ContextVar[str] = ContextVar("current_realname", default="") + +# 当前请求的链路追踪 ID +current_trace_id: ContextVar[str] = ContextVar("current_trace_id", default="") +``` + +## 工作原理 + +``` +请求进入 + │ + ├─ authentication.py: 解析 JWT + │ ├─ current_user_id.set(user_id) + │ ├─ current_username.set(username) + │ └─ current_realname.set(realname) + ├─ trace_id.py: 生成 TraceID → current_trace_id.set(trace_id) + │ + ├─ Handler → Service → Repository + │ └─ 任意层可通过 current_realname.get() 获取用户姓名 + │ + └─ 请求结束: contextvars 自动清理 +``` + +## 使用场景 + +### 1. Repository 层自动填充审计字段 + +```python +# src/core/base_repository.py +class BaseRepository: + def _uid(self) -> str: + """获取当前登录用户姓名(从上下文变量读取)""" + return current_realname.get() + + def create(self, obj): + # 自动填充创建人(姓名) + if hasattr(obj, "create_user"): + obj.create_user = self._uid() + # ... + + def update(self, pk, data): + # 自动填充更新人(姓名) + data["update_user"] = self._uid() + # ... +``` + +### 2. 操作日志记录 + +```python +# src/common/middleware/operation_log.py +# 操作日志通过异步落库,显式传入 realname(上下文在异步回调中可能已失效) +tornado.ioloop.IOLoop.current().spawn_callback( + record_operation_log, + realname=current_realname.get(), + # ... +) +``` + +### 3. 日志输出中携带上下文 + +```python +# src/core/logger.py +# JSON 日志自动携带 trace_id 和 user_id +{ + "timestamp": "2024-01-15T10:30:00", + "level": "INFO", + "trace_id": current_trace_id.get(), + "user_id": current_user_id.get(), + "message": "..." +} +``` + +## 设置时机 + +### current_user_id / current_username / current_realname + +在 `src/common/middleware/authentication.py` 的 `login_required` 装饰器中设置: + +```python +def login_required(func): + def wrapper(self, *args, **kwargs): + # 解析 JWT Token + result = parse_payload(access_token) + data = result["data"] + + # 设置上下文(userId / username / realname 均从 JWT payload 读取) + current_user_id.set(int(data["userId"])) + current_username.set(data.get("username", "")) + current_realname.set(data.get("realname", "")) + + return func(self, *args, **kwargs) + return wrapper +``` + +:::tip JWT 载荷 +登录签发 JWT 时已将 `userId`、`username`、`realname` 写入 payload,认证中间件解析后一并写入上下文。旧 token 中可能不含 `username` / `realname`,`.get()` 会返回空字符串,重新登录后生效。 +::: + +### current_trace_id + +在 `src/common/middleware/trace_id.py` 的 `apply_trace_id` 中设置: + +```python +def apply_trace_id(handler): + trace_id = uuid.uuid4().hex[:16] + current_trace_id.set(trace_id) + handler.set_header("X-Trace-Id", trace_id) +``` + +## 生命周期 + +| 阶段 | 状态 | +|:-----|:-----| +| 请求进入 | `current_user_id` = 0, `current_username` = "", `current_realname` = "", `current_trace_id` = "" | +| `set_default_headers` | `current_trace_id` 被设置 | +| `prepare` (login_required) | `current_user_id` / `current_username` / `current_realname` 被设置 | +| Handler → Service → Repository | 可随时读取 | +| `on_finish` | contextvars 自动清理(下一个请求重置) | + +## 与 Tornado 的区别 + +| 方面 | Tornado | Tornado | +|:-----|:--------|:--------| +| 实现方式 | `contextvars` + 中间件注入 | `contextvars` + 装饰器注入 | +| DB 会话 | 中间件注入 `contextvars` | `scoped_session` 线程本地 | +| 清理时机 | 中间件 finally | `on_finish` | + +## 总结 + +请求上下文通过 `contextvars` 实现请求级别的全局变量传递。`current_user_id` / `current_username` / `current_realname` 在认证装饰器中设置,`current_trace_id` 在请求头处理中设置。Repository 层自动读取 `current_realname` 填充审计字段(`create_user` / `update_user` 写入操作者姓名),日志系统自动读取上下文变量输出链路信息。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/db-session.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/db-session.md new file mode 100644 index 0000000..76365dc --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/db-session.md @@ -0,0 +1,86 @@ +# 数据库会话 + +:::tip 说明 +数据库会话通过 `scoped_session` 实现线程级隔离,Repository 层的写操作自动 commit,`BaseHandler.on_finish()` 兜底关闭会话。核心代码位于 `src/core/database.py`。 +::: + +## 核心架构 + +数据库会话管理位于 `src/core/database.py`。 + +### scoped_session 隔离 + +```python +from sqlalchemy.orm import scoped_session, sessionmaker + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +db = scoped_session(SessionLocal) +``` + +`scoped_session` 基于 thread-local 实现线程级隔离,每个线程持有独立的数据库会话,互不干扰。 + +### 会话使用 + +业务代码通过 `BaseRepository` 的 `self.db` 属性获取当前会话: + +```python +class BaseRepository: + def __init__(self, db=None): + self.db = db or _default_db # 默认使用 scoped_session +``` + +## 会话生命周期 + +### 创建 + +Repository 实例化时自动获取会话(`scoped_session` 按线程自动创建)。 + +### 提交 + +Repository 层的写操作内部自动 `db.commit()`: + +```python +class BaseRepository: + def create(self, obj): + if hasattr(obj, "create_user"): + obj.create_user = str(self._uid()) + self.db.add(obj) + self.db.commit() # 自动提交 + return obj + + def update(self, pk, data): + data["update_user"] = str(self._uid()) + data["update_time"] = datetime.now() + result = self.db.query(self.model).filter_by(id=pk).update(data) + self.db.commit() # 自动提交 + return bool(result) +``` + +### 关闭 + +Service 层的 `try/finally` 块确保 `repo.close()` 释放会话: + +```python +class BaseService: + async def get_page(self, handler): + try: + # ... 业务逻辑 ... + return R.page(handler, data=result, total=total, current=pageNo, size=pageSize) + finally: + self.repo.close() # 释放会话 +``` + +`BaseHandler.on_finish()` 兜底关闭会话,避免长连接泄漏: + +```python +class BaseHandler(tornado.web.RequestHandler): + def on_finish(self): + try: + db.close() # 兜底关闭(Service 层已关闭时为幂等 no-op) + except Exception: + pass +``` + +## 总结 + +数据库会话通过 `scoped_session` 实现线程级隔离,Repository 层写操作自动 `commit()`,Service 层 `try/finally` 确保 `close()`,`BaseHandler.on_finish()` 兜底关闭。业务代码无需手动管理事务。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/dict-util.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/dict-util.md new file mode 100644 index 0000000..b9750be --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/dict-util.md @@ -0,0 +1,160 @@ +# 字典缓存工具 + +:::tip 说明 +`src/common/utils/dict_util.py` 提供业务字典的缓存读取能力,从 Redis 缓存中获取字典数据,缓存未命中时从数据库加载。字典维护时自动失效缓存。 +::: + +## 核心接口 + +### get_dict_data — 获取字典下拉数据 + +```python +from common.utils.dict_util import get_dict_data + +# 获取字典编码对应的下拉选项列表 +options = get_dict_data("link_type") +# 返回: [{"label": "友情链接", "value": "1"}, {"label": "合作伙伴", "value": "2"}] +``` + +### get_dict_label — 获取字典项标签 + +```python +from common.utils.dict_util import get_dict_label + +# 根据字典编码和值获取显示标签 +label = get_dict_label("link_type", "1") +# 返回: "友情链接" +``` + +### get_dict_map — 获取字典值→标签映射 + +```python +from common.utils.dict_util import get_dict_map + +# 获取字典编码的值→标签映射字典 +mapping = get_dict_map("link_type") +# 返回: {"1": "友情链接", "2": "合作伙伴"} +``` + +## 缓存策略 + +``` +1. 读取 Redis 缓存: dict:{code} + ├─ 命中 → 直接返回 + └─ 未命中 → 从 DB 查询 → 写入 Redis → 返回 +2. 字典维护(增删改)→ 删除 Redis 缓存 → 下次读取时自动重建 +``` + +### Redis Key 设计 + +| Key | 说明 | TTL | +|:----|:-----|:----| +| `dict:{code}` | 字典编码对应的选项列表 | 不过期(主动失效) | +| `dict:version` | 字典版本号(缓存失效触发器) | 不过期 | + +### 缓存失效 + +字典数据变更时,通过递增版本号使所有缓存失效: + +```python +def invalidate_dict_cache(): + """字典数据变更时调用,递增版本号使缓存失效。""" + redis = get_redis() + redis.incr("dict:version") +``` + +## 在 BaseService 中使用 + +通过 `serialize_maps` 声明字段→字典编码映射,序列化时自动转换: + +```python +class LinkService(BaseService): + serialize_maps = { + "type": "link_type", # type 字段按 link_type 字典解析 + "form": "link_form", # form 字段按 link_form 字典解析 + } +``` + +序列化时自动将 `type="1"` 转换为 `type_label="友情链接"`。 + +## 与字典模块的关系 + +| 模块 | 职责 | +|:-----|:-----| +| `modules/dictionary/` | 字典数据的 CRUD 维护 | +| `common/utils/dict_util.py` | 字典数据的缓存读取 | + +字典维护接口在增删改时自动调用 `invalidate_dict_cache()`,读取端无需关心缓存一致性。 + +## 使用场景 + +### 场景一:Service 序列化自动转换 + +通过 `serialize_maps` 声明字段→字典编码映射,列表/详情序列化时自动追加 `xxxText` 字段: + +```python +class LinkService(BaseService): + serialize_maps = { + "type": "link_type", # type → typeText + "form": "link_form", # form → formText + } +``` + +序列化结果: + +```json +{ + "id": 1, + "name": "小蚂蚁云", + "type": "1", + "typeText": "友情链接", + "form": "2", + "formText": "后台添加" +} +``` + +### 场景二:Handler 下拉接口 + +```python +class DictDataHandler(BaseHandler): + async def get(self, code): + """GET /api/v1/dict/data/{code}""" + items = get_dict_data(code) + return R.ok(self, data=items) +``` + +前端调用: + +```javascript +const res = await request({ url: '/api/v1/dict/data/link_type' }); +// res.data: [{"label": "友情链接", "value": "1"}, {"label": "合作伙伴", "value": "2"}] +``` + +### 场景三:后端业务校验 + +```python +def _before_add(self, handler, data): + """新增前校验:字典值是否合法""" + valid_types = get_dict_map("link_type") + if str(data.get("type")) not in valid_types: + raise ValidationError("链接类型不合法") +``` + +### 场景四:导出 Excel 翻译 + +```python +def get_export_data(self, handler): + items = self.repo.all() + type_map = get_dict_map("link_type") + result = [] + for item in items: + result.append({ + "name": item.name, + "type": type_map.get(str(item.type), "未知"), # 数字→中文 + }) + return result +``` + +## 总结 + +字典缓存工具提供 `get_dict_data`、`get_dict_label`、`get_dict_map` 三个核心接口,基于 Redis 缓存实现高性能读取。字典维护时通过版本号机制自动失效缓存,保证数据一致性。广泛用于 Service 序列化、下拉接口、业务校验、导出翻译等场景。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/excel.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/excel.md new file mode 100644 index 0000000..60b0b68 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/excel.md @@ -0,0 +1,119 @@ +# 导入导出 + +:::tip 说明 +基于 `openpyxl` 实现 Excel 导入导出功能,支持通过 `file_template` 模块管理导入模板配置。端点定义在各业务模块的 endpoint 文件中(如 `src/modules/user.py`)。 +::: + +## 功能概述 + +Excel 导入导出是后台管理系统的常见需求,通过 `openpyxl` 库实现: + +``` +Excel 导出: +1. 将数据库中的数据导出为 Excel 文件,支持自定义表头和格式 +2. 适用于生成报表、数据备份等场景 + +Excel 导入: +1. 将 Excel 文件中的数据导入到数据库,支持批量处理和校验 +2. 适用于批量数据录入、数据迁移等场景 +``` + +## 依赖说明 + +在 `requirements.txt` 中已包含: + +``` +openpyxl>=3.1.0 +``` + +## 导出实现 + +导出接口通常在各业务模块的 Handler 中定义,返回文件流: + +```python +from openpyxl import Workbook +import io + +class UserExportHandler(BaseHandler): + @permission_required("sys:user:export") + async def get(self): + # 查询数据 + data_list = user_service.get_export_data(self) + + # 创建工作簿 + wb = Workbook() + ws = wb.active + ws.title = "用户信息" + + # 写入表头 + ws.append(["用户名", "姓名", "手机号", "邮箱", "状态"]) + + # 写入数据 + for item in data_list: + ws.append([item.username, item.realname, item.phone, item.email, item.status_name]) + + # 返回文件流 + output = io.BytesIO() + wb.save(output) + output.seek(0) + + self.set_header("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + self.set_header("Content-Disposition", "attachment; filename=export.xlsx") + self.finish(output.getvalue()) +``` + +## 导入实现 + +导入接口接收上传的 Excel 文件,解析后逐行校验并入库: + +```python +class UserImportHandler(BaseHandler): + @permission_required("sys:user:import") + @check_demo + async def post(self): + file = self.request.files.get('file', [None])[0] + if not file: + return R.failed(self, "请上传文件") + wb = load_workbook(io.BytesIO(file.body)) + ws = wb.active + + # 跳过表头,逐行解析 + for row in ws.iter_rows(min_row=2, values_only=True): + username, realname, phone, email, status = row + # 校验并入库 ... +``` + +## file_template 模块 + +`file_template` 模块用于管理导入模板的配置信息,定义模板的字段映射、校验规则等。 + +模块路径:`src/modules/file_template/` + +``` +模板配置包含: +- 模板名称 +- 关联模块 +- 字段映射关系 +- 校验规则 +``` + +:::tip 温馨提示 +导入时建议先下载模板文件,按模板格式填写数据后再导入,可减少格式错误。 +::: + +## 错误处理 + +导入过程中遇到校验错误时,返回详细的错误信息: + +```json +{ + "code": 1, + "msg": "导入失败:第3行用户名已存在,第5行手机号格式错误", + "data": null, + "ok": false +} +``` + +## 总结 + +Excel 导入导出功能基于 `openpyxl` 实现,导出通过 `self.finish()` 返回文件流避免内存溢出,导入逐行校验并提供详细错误信息。`file_template` 模块可管理导入模板配置,便于维护字段映射和校验规则。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/helpers.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/helpers.md new file mode 100644 index 0000000..9d04b7b --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/helpers.md @@ -0,0 +1,75 @@ +# 通用 Helper 函数 + +:::tip 说明 +`src/common/utils/helpers.py` 提供项目中广泛使用的通用工具函数,包括 ID 解析、OS/浏览器检测、参数脱敏等。 +::: + +## parse_id_list — ID 列表解析 + +将逗号分隔的 ID 字符串解析为整数列表: + +```python +from common.utils.helpers import parse_id_list + +parse_id_list("1,2,3") +# 返回: [1, 2, 3] + +parse_id_list("1,2,abc,3") +# 返回: [1, 2, 3] # 自动跳过非数字项 + +parse_id_list("") +# 返回: [] +``` + +## detect_os — 操作系统检测 + +从 User-Agent 字符串提取操作系统信息: + +```python +from common.utils.helpers import detect_os + +detect_os("Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...") +# 返回: "Windows 10.0" +``` + +支持 Windows、macOS、Android、iOS、Linux 等主流操作系统。 + +## detect_browser — 浏览器检测 + +从 User-Agent 字符串提取浏览器信息: + +```python +from common.utils.helpers import detect_browser + +detect_browser("Mozilla/5.0 ... Chrome/120.0.0.0 ...") +# 返回: "Chrome 120.0" +``` + +支持 Edge、Chrome、Firefox、Safari、IE 等主流浏览器。Edge 优先匹配,避免被误判为 Chrome。 + +## redact_params — 参数脱敏 + +对敏感参数进行脱敏处理,用于操作日志记录: + +```python +from common.utils.helpers import redact_params + +params = {"username": "admin", "password": "123456", "name": "张三"} +redact_params(params) +# 返回: {"username": "admin", "password": "******", "name": "张三"} +``` + +自动对 `password`、`salt`、`token`、`secret` 等敏感字段脱敏。 + +## 使用场景 + +| 函数 | 主要使用场景 | +|:-----|:------------| +| `parse_id_list` | 批量删除、批量状态变更 | +| `detect_os` | 操作日志、登录日志 | +| `detect_browser` | 操作日志、登录日志 | +| `redact_params` | 操作日志参数记录 | + +## 总结 + +通用 Helper 函数覆盖了 ID 解析、设备检测、参数脱敏等常见场景。这些函数在各业务模块中广泛使用,是项目的基础工具集。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/ip-region.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/ip-region.md new file mode 100644 index 0000000..ddbfea8 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/ip-region.md @@ -0,0 +1,133 @@ +# IP 归属地解析 + +:::tip 说明 +`src/common/utils/ip2region.py` 基于 [ip2region](https://github.com/lionsoul2014/ip2region) 离线数据库,提供 IP 地址到省市区的归属地查询能力,无需网络请求。`src/common/utils/helpers.py` 中的 `detect_os` 和 `detect_browser` 提供 User-Agent 解析。 +::: + +## 核心接口 + +### get_ip_location — 获取归属地文本 + +```python +from common.utils.ip2region import get_ip_location + +location = get_ip_location("114.114.114.114") +# 返回: "中国 江苏省 南京市" + +location = get_ip_location("8.8.8.8") +# 返回: "United States California" + +location = get_ip_location("10.0.0.1") +# 返回: "内网IP" +``` + +### get_ip_region — 获取完整区域信息 + +```python +from common.utils.ip2region import get_ip_region + +info = get_ip_region("114.114.114.114") +# 返回: {"country": "中国", "province": "江苏省", "city": "南京市", "isp": "南京信风", "country_code": "CN"} +``` + +## UA 解析 + +位于 `src/common/utils/helpers.py`: + +```python +from common.utils.helpers import detect_os, detect_browser + +# 解析操作系统 +os_name = detect_os("Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...") +# 返回: "Windows 10.0" + +# 解析浏览器 +browser_name = detect_browser("Mozilla/5.0 ... Chrome/120.0.0.0 ...") +# 返回: "Chrome 120.0" +``` + +## 应用场景 + +### 操作日志记录 + +```python +# src/common/middleware/operation_log.py +ip = get_ip_location(handler.request.remote_ip) +os_name = detect_os(ua) +browser_name = detect_browser(ua) +``` + +### 登录日志记录 + +```python +# src/common/middleware/login_log.py +ip = get_ip_location(handler.request.remote_ip) +os_name = detect_os(ua) +browser_name = detect_browser(ua) +``` + +## 预热机制 + +ip2region.xdb 文件约 10MB,首次查询时加载。应用启动时预热避免首次请求延迟: + +```python +# src/bootstrap.py +from common.utils.ip2region import preload +preload() # 预加载 xdb 文件到内存 +``` + +## 降级策略 + +| 异常场景 | 降级行为 | +|:---------|:---------| +| xdb 文件不存在 | 返回 `"未知"` | +| xdb 加载失败 | 返回 `"未知"` | +| 畸形 IP 地址 | 返回 `"未知"` | +| 内网/保留地址 | 返回 `"内网IP"` | + +所有查询接口均采用 fail-open 降级策略,查询失败绝不向上抛异常,保证日志落库不受影响。 + +## xdb 数据库更新 + +ip2region.xdb 离线数据库需要定期更新以覆盖新增的 IP 段: + +```bash +# 1. 下载最新 xdb 文件 +# GitHub: https://github.com/lionsoul2014/ip2region/tree/master/data +# 将 ip2region.xdb 放到 static/ 目录下 + +# 2. 重启应用即可生效(启动时自动加载) +poetry run python app.py --port=8041 +``` + +:::tip 数据库格式 +xdb 是 ip2region 的二进制索引格式,约 10MB,支持微秒级查询。文件路径在 `src/common/utils/ip2region.py` 中配置。 +::: + +## 完整日志记录示例 + +操作日志和登录日志中记录完整的客户端信息: + +```python +from common.utils.ip2region import get_ip_location +from common.utils.helpers import detect_os, detect_browser + +# 获取客户端信息 +ip = handler.request.remote_ip +ua = handler.request.headers.get("User-Agent", "") +location = get_ip_location(ip) +os_name = detect_os(ua) +browser_name = detect_browser(ua) + +# 记录到日志 +log_data = { + "ip": ip, # "114.114.114.114" + "ip_location": location, # "中国 江苏省 南京市" + "os": os_name, # "Windows 10.0" + "browser": browser_name, # "Chrome 120.0" +} +``` + +## 总结 + +IP 归属地解析基于 ip2region 离线数据库,零网络依赖。提供 `get_ip_location`(文本)和 `get_ip_region`(结构化)两个查询接口,配合 `detect_os` / `detect_browser` 为操作日志和登录日志提供完整的客户端身份信息。xdb 文件需定期更新以覆盖新增 IP 段。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/logger.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/logger.md new file mode 100644 index 0000000..1be85a4 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/logger.md @@ -0,0 +1,124 @@ +# 日志系统 + +:::tip 说明 +通过 Python 标准库 `logging` 实现结构化日志,支持控制台 + 文件双输出,日志文件按大小自动轮转。配置入口位于 `src/core/logger.py`。 +::: + +## 配置入口 + +日志配置位于 `src/core/logger.py`: + +```python +import logging +import sys +from logging.handlers import RotatingFileHandler +from pathlib import Path + +# 日志目录 +LOG_DIR = Path(__file__).resolve().parents[2] / "logs" +LOG_DIR.mkdir(parents=True, exist_ok=True) + +# 日志格式 +_FORMAT = "%(asctime)s | %(levelname)-7s | %(name)s | %(message)s" + +# 轮转配置 +LOG_MAX_BYTES = 10 * 1024 * 1024 # 10MB +LOG_BACKUP_COUNT = 5 +``` + +## setup_logging() + +```python +def setup_logging(level: int = logging.INFO, log_file: str = "app.log") -> None: + """ + 配置全局日志:root logger 输出到控制台与文件 + + Args: + level: 日志级别(默认 INFO) + log_file: 日志文件名(位于 logs/ 目录下) + """ +``` + +### 控制台输出 + +```python +console = logging.StreamHandler(sys.stdout) +console.setFormatter(formatter) +root.addHandler(console) +``` + +### 文件输出(按大小轮转) + +```python +file_handler = RotatingFileHandler( + LOG_DIR / log_file, + maxBytes=LOG_MAX_BYTES, # 单文件上限 10MB + backupCount=LOG_BACKUP_COUNT, # 保留 5 份备份 + encoding="utf-8", +) +``` + +## 日志轮转 + +当日志文件达到 10MB 时自动轮转: + +``` +logs/app.log ← 当前日志 +logs/app.log.1 ← 最近一次轮转 +logs/app.log.2 ← 倒数第二次 +logs/app.log.3 +logs/app.log.4 +logs/app.log.5 ← 最早的备份(超过自动删除) +``` + +## 日志格式 + +``` +2025-03-06 14:30:25,123 | INFO | user.service | 用户登录成功 +``` + +格式说明: + +| 字段 | 说明 | +|------|------| +| `asctime` | 时间戳 | +| `levelname` | 日志级别(右对齐7字符) | +| `name` | 模块名称 | +| `message` | 日志内容 | + +## 使用方式 + +在各模块中获取 logger: + +```python +import logging + +logger = logging.getLogger(__name__) + +logger.info("操作成功") +logger.warning("配置缺失") +logger.error(f"异常信息: {str(e)}") +``` + +## 框架日志集成 + +Tornado 的默认日志统一走 root logger: + +```python +for name in ("tornado.httpserver", "tornado"): + lg = logging.getLogger(name) + lg.handlers.clear() + lg.propagate = True +``` + +:::tip 温馨提示 +`setup_logging()` 会检查是否已添加 handler(通过 `_djangoadmin` 标记),避免重复添加。可在应用启动时调用一次。 +::: + +## 操作日志 + +除了应用日志外,还通过 `src/common/middleware/operation_log.py` 中间件自动记录用户的写操作日志,存储到数据库 `tornado_operation_log` 表中。详见 [操作日志](/zh/volume4/core/operation-log) 章节。 + +## 总结 + +日志系统通过 `RotatingFileHandler` 实现按大小轮转,单文件上限 10MB 保留 5 份备份,防止日志无限增长撑爆磁盘。控制台和文件双输出,格式统一包含时间戳、级别、模块名和消息内容。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/password.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/password.md new file mode 100644 index 0000000..c47707e --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/password.md @@ -0,0 +1,120 @@ +# 密码工具 + +:::tip 说明 +采用 bcrypt 双重加盐方案存储密码,源码位于 `src/core/password.py`。工作因子 12(约 250ms),在安全性和性能之间取得平衡。 +::: + +## 加密方案 + +密码存储采用「外部随机盐 + bcrypt 内部盐」双重加盐方案: + +``` +用户密码 + 外部盐 → bcrypt.hashpw → 存储哈希值 +``` + +### 生成盐值 + +```python +import secrets + +BCRYPT_ROUNDS = 12 # 工作因子,约250ms + +def generate_salt(length: int = 10) -> str: + """生成随机盐值""" + return secrets.token_urlsafe(length)[:length] +``` + +### 加密密码 + +```python +import bcrypt + +def encrypt_password(password: str, salt: str) -> str: + """使用 bcrypt 加密密码(外部 salt 双重加盐)""" + combined = (password + salt).encode('utf-8') + hashed = bcrypt.hashpw(combined, bcrypt.gensalt(rounds=BCRYPT_ROUNDS)) + return hashed.decode('utf-8') +``` + +### 验证密码 + +```python +def verify_password(password: str, salt: str, hashed: str) -> bool: + """验证密码是否正确""" + combined = (password + salt).encode('utf-8') + return bcrypt.checkpw(combined, hashed.encode('utf-8')) +``` + +## 工作因子 + +`BCRYPT_ROUNDS = 12` 表示 bcrypt 的工作因子为 12,即需要 2^12 = 4096 次迭代。值越大计算越慢越安全,12 是当前推荐值(约 250ms)。 + +:::tip 温馨提示 +bcrypt 的工作因子决定了加密速度。增加 1 会使计算时间翻倍。建议保持默认值 12,在安全性和性能之间取得平衡。 +::: + +## 存储结构 + +用户表中密码相关字段: + +| 字段 | 说明 | +|------|------| +| `password` | bcrypt 哈希后的密码 | +| `salt` | 外部随机盐值 | + +## 使用示例 + +### 注册/创建用户 + +```python +from core.password import generate_salt, encrypt_password + +salt = generate_salt() +hashed = encrypt_password("mypassword", salt) +# 将 hashed 和 salt 存入数据库 +``` + +### 登录验证 + +登录验证位于 `src/modules/auth/service.py`,bcrypt 为 CPU 密集操作,使用 `asyncio.to_thread` 放入线程池避免阻塞事件循环: + +```python +from core.password import verify_password + +# 从数据库获取用户记录 +user = get_user_by_username(username) + +# bcrypt 为 CPU 密集操作,放入线程池 +password_ok = await asyncio.to_thread( + password.verify_password, data.password, user.salt or '', user.password or '' +) +if password_ok: + # 密码正确 + ... +``` + +### 修改密码 + +```python +# 验证旧密码 +if not verify_password(old_password, user.salt, user.password): + return R.failed("原密码错误") + +# 生成新盐值并加密 +new_salt = generate_salt() +new_hashed = encrypt_password(new_password, new_salt) +# 更新数据库 +``` + +## 安全特性 + +``` +1. 双重加盐:外部随机盐 + bcrypt 内部盐,即使两个用户使用相同密码,存储的哈希值也不同 +2. 抗彩虹表:bcrypt 自带盐值,无法预计算彩虹表 +3. 抗暴力破解:工作因子 12 使每次验证约 250ms,大幅增加暴力破解成本 +4. 自适应性:工作因子可随硬件提升而增加 +``` + +## 总结 + +密码工具采用 bcrypt 双重加盐方案,外部盐值由 `secrets.token_urlsafe()` 生成,bcrypt 内部盐自动处理。工作因子 12 在安全性和性能之间取得平衡,有效抵御彩虹表和暴力破解攻击。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/perm-cache.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/perm-cache.md new file mode 100644 index 0000000..029ec3d --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/perm-cache.md @@ -0,0 +1,154 @@ +# 权限缓存 + +:::tip 说明 +通过 Redis 实现用户权限和菜单的缓存,采用用户级缓存 + 全局版本号双层策略。源码位于 `src/common/permission.py`,供 `src/modules/menu/service.py` 中的权限查询函数调用。 +::: + +## 缓存策略 + +采用「用户级缓存 + 全局版本号」双层策略: + +``` +1. 每个用户的缓存 Key 带版本号后缀 +2. 版本号存全局 Key(perm:version) +3. 菜单变更时递增全局版本号,所有用户缓存自然失效 +4. 用户角色变更时,直接删除该用户缓存 +5. 角色菜单变更时,查询该角色下所有用户 ID,批量删除缓存 +``` + +## 缓存 Key 结构 + +```python +_VERSION_KEY = 'perm:version' # 全局版本号 +_PERM_PREFIX = 'perm:list:' # 用户权限列表 +_MENU_PREFIX = 'perm:menu:' # 用户菜单树 +_USER_ACTIVE_PREFIX = 'user:active:' # 用户有效状态 +``` + +Key 格式示例: + +``` +perm:version → 5 +perm:list:1001:5 → ["sys:user:page", "sys:user:add", ...] +perm:menu:1001:5 → [{id: 1, name: "系统管理", children: [...]}] +user:active:1001 → "1" +``` + +## 权限缓存读写 + +### get_cached_permissions() + +```python +async def get_cached_permissions(user_id: int): + """获取缓存的用户权限列表,未命中返回 None""" + redis = get_redis() + ver = await _get_version() + key = f'{_PERM_PREFIX}{user_id}:{ver}' + data = redis.get(key) + if data is not None: + return json.loads(data) + return None +``` + +### set_cached_permissions() + +```python +async def set_cached_permissions(user_id: int, perm_list: list): + """缓存用户权限列表(空列表不缓存,避免缓存空壳后无法感知后续数据变更)""" + if not perm_list: + return + redis = get_redis() + ver = await _get_version() + key = f'{_PERM_PREFIX}{user_id}:{ver}' + redis.set(key, json.dumps(perm_list), ex=_CACHE_TTL) +``` + +## 缓存失效 + +### 菜单变更 + +```python +async def invalidate_menu_change(): + """菜单增删改时调用,递增全局版本号使所有用户缓存失效""" + await _incr_version() +``` + +递增版本号后,旧版本的缓存 Key 不再被访问,自然过期(TTL 2小时)。 + +### 用户角色变更 + +```python +async def invalidate_user_perms(user_id: int): + """用户角色变更时调用,删除该用户的权限和菜单缓存""" + redis = get_redis() + ver = await _get_version() + keys = [ + f'{_PERM_PREFIX}{user_id}:{ver}', + f'{_MENU_PREFIX}{user_id}:{ver}', + ] + redis.delete(*keys) +``` + +### 角色菜单变更 + +```python +async def invalidate_role_perms(role_id: int): + """角色菜单变更时调用,清理该角色下所有用户的权限和菜单缓存""" + # 查询该角色下所有用户 ID + user_ids = await asyncio.to_thread(_query_user_ids) + + # 批量删除缓存 + redis = get_redis() + ver = await _get_version() + keys = [] + for uid in user_ids: + keys.append(f'{_PERM_PREFIX}{uid}:{ver}') + keys.append(f'{_MENU_PREFIX}{uid}:{ver}') + redis.delete(*keys) +``` + +## 用户有效状态缓存 + +用于 `login_required` 每请求校验,避免每次请求都查库: + +```python +_USER_ACTIVE_TTL = 300 # 5分钟 + +async def get_cached_user_active(user_id: int): + """读取用户有效状态缓存;未命中返回 None(调用方需回源 DB 校验)""" + +async def set_cached_user_active(user_id: int, active: bool): + """写入用户有效状态缓存""" + +async def invalidate_user_active(user_id: int): + """用户状态/删除变更时调用,删除有效状态缓存""" +``` + +## 异常降级 + +所有缓存操作都包裹 try/except,Redis 不可用时降级直查 DB: + +```python +except Exception as e: + logger.warning(f"读取权限缓存异常: {e}") + return None # 调用方回源 DB +``` + +删除失败时递增全局版本号孤立旧缓存(fail-closed): + +```python +except Exception as e: + logger.error(f"清理用户权限缓存异常,降级递增全局版本号: {e}") + try: + await _incr_version() + except Exception as e2: + logger.error(f"降级递增版本号失败: {e2}") +``` + +:::tip 温馨提示 +权限缓存的 TTL 为 2小时,用户有效状态缓存 TTL 为 5分钟。正常情况下缓存由业务操作即时失效,TTL 仅作为兜底防止 Redis 故障时缓存永久残留。 +::: + +## 总结 + +权限缓存采用用户级缓存 + 全局版本号双层策略,菜单变更通过递增版本号使所有缓存自然失效,无需遍历删除。所有缓存操作都有异常降级机制,Redis 不可用时自动回源数据库查询。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/response.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/response.md new file mode 100644 index 0000000..fdf22b8 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/response.md @@ -0,0 +1,154 @@ +# 统一响应封装(R 对象) + +:::tip 说明 +`src/core/response.py` 提供统一的 API 响应封装,所有接口返回格式为 `{code, data, msg, ok}`。R 对象是全局统一的响应工具类。 +::: + +## 响应格式 + +```json +{ + "code": 0, + "data": {}, + "msg": "操作成功", + "ok": true +} +``` + +| 字段 | 类型 | 说明 | +|:-----|:-----|:-----| +| `code` | int | 状态码:0=成功,非0=失败 | +| `data` | any | 响应数据 | +| `msg` | string | 提示信息 | +| `ok` | bool | 是否成功 | + +## R 对象方法 + +### R.ok — 成功响应 + +```python +from core import response as R + +# 基本成功 +R.ok(handler) + +# 带数据 +R.ok(handler, data={"id": 1, "name": "admin"}) + +# 自定义消息 +R.ok(handler, msg="注册成功") + +# 带分页 +R.ok(handler, data={"list": [...], "total": 100}) +``` + +### R.failed — 失败响应 + +```python +# 基本失败 +R.failed(handler) + +# 自定义消息 +R.failed(handler, msg="用户名已存在") + +# 自定义状态码 +R.failed(handler, code=401, msg="未授权") +``` + +### R.response — 自定义响应 + +```python +# 完全自定义 +R.response(handler, code=0, data={}, msg="自定义", ok=True) +``` + +## 实现原理 + +```python +# src/core/response.py +from core.base_handler import BaseHandler + +class R: + @staticmethod + def ok(handler: BaseHandler, data=None, msg="操作成功"): + handler.finish({ + "code": Codes.SUCCESS, + "data": data, + "msg": msg, + "ok": True, + }) + + @staticmethod + def failed(handler: BaseHandler, msg="操作失败", code=Codes.FAILED): + handler.finish({ + "code": code, + "data": None, + "msg": msg, + "ok": False, + }) + + @staticmethod + def response(handler: BaseHandler, code=0, data=None, msg="", ok=True): + handler.finish({ + "code": code, + "data": data, + "msg": msg, + "ok": ok, + }) +``` + +## Codes 状态码 + +```python +class Codes: + SUCCESS = 0 # 成功 + FAILED = 1 # 业务失败 + UNAUTHORIZED = 401 # 未授权 + FORBIDDEN = 403 # 无权限 + NOT_FOUND = 404 # 资源不存在 + SERVER_ERROR = 500 # 服务器错误 +``` + +## 使用规范 + +### Handler 层使用 + +```python +class LinkDetailHandler(BaseHandler): + @permission_required("sys:link:detail") + async def get(self, id): + # Service 返回原始数据时,用 R.ok 包装 + return R.ok(self, data=link_service.get_detail(id)) +``` + +### Service 层使用 + +```python +class LinkService(BaseService): + async def add(self, handler): + # Service 返回 R 对象 + # ... + return R.ok(handler, msg="添加成功") +``` + +### 异常处理中的使用 + +```python +# src/core/base_handler.py +def write_error(self, status_code, **kwargs): + exc = kwargs.get("exc_info", (None, None, None))[1] + if isinstance(exc, BaseAppError): + self.finish({"code": exc.code, "data": None, "msg": exc.msg, "ok": False}) +``` + +## 与 Tornado 的区别 + +| 方面 | 说明 | +|:-----|:-----| +| 响应方式 | `R.ok(self, data=...)` / `R.failed(self, msg=...)` | +| 底层实现 | `self.finish(body)` 写出 JSON | +| Handler 参数 | `self`(Handler 实例) | + +## 总结 + +R 对象是统一响应封装的核心工具,所有 API 返回 `{code, data, msg, ok}` 格式。成功用 `R.ok()`,失败用 `R.failed()`,自定义用 `R.response()`。Handler 层直接调用 `R.ok(self, data=...)` 完成响应。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/rich-text.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/rich-text.md new file mode 100644 index 0000000..b39d4dd --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/rich-text.md @@ -0,0 +1,124 @@ +# 富文本过滤 + +:::tip 说明 +富文本编辑器内容存在存储型 XSS 风险,通过 `bleach` 库实现 HTML 白名单清洗,在落盘前统一过滤危险内容。源码位于 `src/common/utils/rich_text.py`,涉及富文本的模块(文章、通知公告等)均调用 `save_content()` 处理。 +::: + +## 安全威胁 + +富文本编辑器允许用户输入 HTML 内容,攻击者可能注入: + +``` +1. +``` + +### 图片预览 + +```vue + + + +``` + +## 业务模块中的文件字段 + +在 Service 中声明 `file_fields`,基类自动处理文件迁移和 URL 补全: + +```python +class ArticleService(BaseService(Article]): + # 文件字段:add/update 时自动迁移临时文件,列表/详情自动补全 URL + file_fields = ('cover', 'attachment') +``` + +### 自动处理逻辑 + +1. **新增/更新时**:检测文件字段值是否为临时路径,如果是则迁移到正式目录 +2. **列表/详情时**:自动为文件字段补全 `TORNADO_FILE_URL` 前缀 + +## 富文本中的图片 + +在 Service 中声明 `rich_text_fields`,基类自动处理: + +```python +class ArticleService(BaseService(Article]): + # 富文本字段:自动迁移嵌入图片 + XSS 清洗 + rich_text_fields = ('content',) +``` + +### 自动处理逻辑 + +1. **新增/更新时**:解析富文本中的 `` 标签 +2. 将临时路径的图片迁移到正式目录 +3. 替换富文本中的图片路径 +4. 进行 XSS 清洗(使用 bleach 库) + +## 常见问题 + +### 上传报 413 Request Entity Too Large + +**原因**:文件大小超过 `UPLOAD_MAX_SIZE_MB` 限制。 + +**解决**:修改 `.env` 中的 `UPLOAD_MAX_SIZE_MB` 值。 + +### 上传报 403 Forbidden + +**原因**:文件扩展名不在白名单中。 + +**解决**:检查 `UPLOAD_ALLOWED_EXTS` 配置,添加所需扩展名。 + +### 图片上传成功但不显示 + +**原因**:`TORNADO_FILE_URL` 配置错误或前端未拼接域名。 + +**解决**:检查 `.env` 中的 `TORNADO_FILE_URL` 配置,确保前端正确拼接。 + +### 上传报演示环境无权限 + +**原因**:演示模式下写操作被拦截。 + +**解决**:将 `.env` 中的 `TORNADO_DEMO` 设为 `False`。 + +## 总结 + +文件上传功能通过扩展名白名单 + 魔术字节校验 + 路径遍历防护 + 大小限制四层安全机制保障安全性。文件按日期分目录存储,使用时间戳 + 随机数重命名。统一的 `uploadFile` 接口支持图片和各类文件上传。Service 层通过 `file_fields` 和 `rich_text_fields` 声明即可自动处理文件迁移和 URL 补全。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/upload.md b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/upload.md new file mode 100644 index 0000000..3c97746 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume4/tools/upload.md @@ -0,0 +1,128 @@ +# 文件上传 + +:::tip 说明 +提供本地文件上传功能,支持图片和通用文件上传,内置扩展名白名单、大小限制、魔术字节校验和路径遍历防护。服务层位于 `src/modules/upload/service.py`,端点位于 `src/modules/upload.py`。 +::: + +## 功能概述 + +文件上传模块提供统一的文件上传接口 `upload_file`,支持图片、文档、压缩包等各类文件,通过 `UPLOAD_ALLOWED_EXTS` 配置控制允许上传的格式。 + +``` +POST /api/v1/upload/uploadFile —— 统一文件上传入口 +``` + +## 配置项 + +在 `.env` 中配置上传相关参数: + +```bash +# 允许上传的文件扩展名(逗号分隔,留空使用内置智能集合) +UPLOAD_ALLOWED_EXTS=.jpg,.png,.gif,.bmp,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.zip,.rar + +# 单文件大小上限(MB) +UPLOAD_MAX_SIZE_MB=10 + +# 上传文件存储根目录 +TORNADO_UPLOAD_DIR=E:\upload + +# 文件访问域名(Nginx 或本地代理) +TORNADO_FILE_URL=http://127.0.0.1:8041/api/file +``` + +:::tip 温馨提示 +`UPLOAD_ALLOWED_EXTS` 配置的扩展名会与内置安全集合取交集,确保不会绕过安全校验。留空时回落到内置智能集合。 +::: + +## 安全机制 + +上传模块内置多层安全防护: + +### 1. 路径遍历防护 + +```python +def _is_safe_filename(filename: str) -> bool: + """拒绝包含路径分隔符或 .. 的文件名""" + normalized = os.path.normpath(filename) + if '..' in normalized.split(os.sep): + return False + basename = os.path.basename(normalized) + return basename == normalized or basename == filename +``` + +### 2. 高危格式黑名单 + +`.exe`、`.bat`、`.sh`、`.ps1`、`.js`、`.vbs`、`.svg`、`.webp` 等高危格式无论配置如何一律拒绝。 + +### 3. 魔术字节校验 + +通过 `verify_file_magic()` 校验文件头部魔术字节,防止伪造扩展名绕过白名单。 + +### 4. 流式大小校验 + +```python +async def _read_upload_limited(file, max_size_bytes): + """流式读取,超过大小上限立即中断,避免整文件读入内存""" + chunks = [] + total = 0 + while True: + chunk = await file.read(65536) + if not chunk: + break + total += len(chunk) + if total > max_size_bytes: + return None, R.failed(msg=f"文件大小超过限制,最大允许 {UPLOAD_MAX_SIZE_MB}MB") + chunks.append(chunk) + return b''.join(chunks), None +``` + +## 文件命名规则 + +上传文件自动重命名,格式为:`年月日时分秒 + 5位随机数 + 扩展名` + +```python +def _generate_filename(ext_with_dot): + timestamp = time.strftime('%Y%m%d%H%M%S') + random_num = '%05d' % random.randint(0, 99999) + return f"{timestamp}{random_num}{ext_with_dot}" +``` + +文件按日期分目录存储:`{UPLOAD_DIR}/{YYYYMMDD}/{文件名}` + +## 接口调用 + +### 上传文件 + +``` +POST /api/v1/upload/uploadFile +Content-Type: multipart/form-data +Authorization: Bearer +参数: file(文件字段) +``` + +响应示例: + +```json +{ + "code": 0, + "msg": "上传成功", + "data": { + "originalName": "photo.jpg", + "fileExtension": "jpg", + "fileType": "image/jpeg", + "fileSize": 306995, + "fileName": "2025030614302512345.jpg", + "filePath": "/20250306/2025030614302512345.jpg", + "fileUrl": "http://127.0.0.1:8041/api/file/20250306/2025030614302512345.jpg" + }, + "ok": true +} +``` + +## 前端集成 + +前端使用 `Upload` 组件上传,上传成功后将返回的 `fileUrl` 存入业务字段。详见 [文件上传联调](/zh/volume4/integration/upload-sync) 章节。 + +## 总结 + +文件上传模块通过扩展名白名单、魔术字节校验、流式大小限制、路径遍历防护等多层安全机制,确保上传功能的安全性。文件自动重命名并按日期分目录存储,避免文件名冲突和信息泄露。统一的 `uploadFile` 接口同时支持图片和各类文件上传。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume5/cli.md b/DjangoAdmin_Tornado_EleVue/zh/volume5/cli.md new file mode 100644 index 0000000..7a7f934 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume5/cli.md @@ -0,0 +1,191 @@ +:::tip 本章概要 +代码生成器 CLI 模式的使用方法,包括单表生成、批量生成、预览模式和配置导出。 +::: + +# CLI 命令行使用 + +CLI 入口为项目根目录的 `generator.py`,内部复用 `src/modules/generator` 下的引擎。适合开发阶段快速生成单个模块。 + +## 基本用法 + +```bash +# 解析表结构并生成模块(最常用) +python generator.py tornado_example +``` + +执行后自动完成: +1. 从数据库解析 `tornado_example` 表结构 +2. 生成后端模块代码到 `src/modules/example/` +3. 生成端点代码到 `src/modules/example.py` +4. 生成前端页面到 `ui/src/views/tool/example/` 和 `ui/src/api/tool/example.ts` +5. 自动注册路由到 `src/api/v1/router.py` +6. 自动创建菜单和权限节点 + +## 预览模式 + +使用 `--dry-run` 仅解析表结构并打印配置摘要,不执行代码生成: + +```bash +python generator.py tornado_example --dry-run +``` + +输出示例: + +``` +============================================================== +数据库表结构解析和代码生成工具 +============================================================== +正在解析表: tornado_example +表结构解析成功 + +------------------------------------------------------------ +生成配置摘要: +------------------------------------------------------------ + 应用名称: example + 模块名称: 案例 + 模型类名: Example + 字段数量: 8 + 是否有排序: True + 是否有状态: True + - name: 案例名称 (String) + - code: 案例编码 (String) + - type: 案例类型 (Integer) + 选项: [(1, '类型1'), (2, '类型2'), (3, '类型3')] + - status: 案例状态 (Integer) + 选项: [(1, '正常'), (2, '停用')] + - sort: 排序 (Integer) + ... 还有 3 个字段 + +============================================================== +预览模式 - 不执行代码生成(去掉 --dry-run 参数即可生成) +============================================================== +``` + +## 导出配置 + +使用 `--output` 将解析结果导出为 JSON 配置文件: + +```bash +# 预览并导出配置 +python generator.py tornado_example --dry-run --output config.json + +# 生成代码并同时保存配置 +python generator.py tornado_example --output config.json +``` + +导出的 `config.json` 包含完整的生成配置,可手动编辑后重新使用。 + +## 使用配置文件生成 + +使用 `--config` 加载已有的 JSON 配置文件,跳过表解析: + +```bash +python generator.py --config config.json +``` + +适用场景: +- 需要微调字段配置后重新生成 +- 表结构已不存在但配置文件保留 +- 跨项目复用配置 + +## 完整参数 + +| 参数 | 简写 | 说明 | +|:----|:----|:----| +| `table_name` | - | 数据库表名(位置参数,可选) | +| `--config` | `-c` | 使用已有的 JSON 配置文件(跳过表解析) | +| `--dry-run` | `-d` | 仅生成配置,不执行代码生成 | +| `--output` | `-o` | 将配置导出为 JSON 文件 | + +## 典型工作流 + +### 快速生成 + +```bash +python generator.py tornado_example +``` + +### 预览 → 微调 → 生成 + +```bash +# 1. 预览配置 +python generator.py tornado_example --dry-run --output config.json + +# 2. 编辑 config.json(调整字段属性、注释等) +vim config.json + +# 3. 用配置文件生成 +python generator.py --config config.json +``` + +### 仅保存配置 + +```bash +python generator.py tornado_example --output config.json +``` + +## 环境要求 + +运行 CLI 前需确保: + +1. **数据库可访问**:CLI 复用 `.env` 中的数据库配置,需确保 MySQL 服务正常且连接信息正确 +2. **Python 依赖已安装**:`pip install -r requirements.txt`(包含 Jinja2、SQLAlchemy 等) +3. **表已存在**:目标表需已在数据库中创建完成 +4. **编码要求**:Windows 环境下确保终端使用 UTF-8 编码(`chcp 65001`) + +:::warning 数据库要求 +CLI 命令行工具通过查询 MySQL 的 `information_schema` 获取表结构信息,**仅支持 MySQL 数据库**。 +::: + +## 常见错误 + +### 表不存在 + +``` +操作失败: 表 tornado_xxx 不存在或无业务字段 +``` + +**原因**:数据库中不存在该表,或表名拼写错误。 + +**解决**:确认表名正确,且表已在数据库中创建。 + +### 目标目录已存在 + +``` +操作失败: 模块 example 已存在,请先删除 src/modules/example +``` + +**原因**:`src/modules/example/` 目录已存在,生成器拒绝覆盖。 + +**解决**:删除已有目录后重新生成(详见 [6.9 常见问题](/zh/volume5/faq))。 + +### 数据库连接失败 + +``` +操作失败: (pymysql.err.OperationalError) (2003, "Can't connect to MySQL server...") +``` + +**原因**:`.env` 中的数据库配置错误,或 MySQL 服务未启动。 + +**解决**:检查 `.env` 中的 `DB_HOST`、`DB_PORT`、`DB_USERNAME`、`DB_PASSWORD` 配置。 + +### 编码错误 + +``` +SyntaxError: Non-UTF-8 code start with '\xc8' in file ... +``` + +**原因**:Windows 终端编码不是 UTF-8。 + +**解决**:执行 `chcp 65001` 切换终端编码。 + +## 注意事项 + +1. **表名支持带前缀**:`tornado_example` 和 `example` 都可以,脚本会自动处理 `tornado_` 前缀 +2. **目标目录不能已存在**:如果 `src/modules/example/` 已存在,会报错提示先删除 +3. **数据库连接**:CLI 复用 `.env` 中的数据库配置,确保数据库可正常访问 +4. **编码要求**:Windows 环境下确保终端使用 UTF-8 编码(`chcp 65001`) + +## 总结 + +CLI 命令行模式通过 `python generator.py` 启动,支持单表生成、批量生成、预览模式和配置导出。生成前自动检测目标目录是否已存在,避免覆盖已有代码。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume5/code-engine.md b/DjangoAdmin_Tornado_EleVue/zh/volume5/code-engine.md new file mode 100644 index 0000000..d88f0ed --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume5/code-engine.md @@ -0,0 +1,201 @@ +:::tip 本章概要 +代码生成引擎的工作原理,包括配置解析、模板渲染、文件写入和自动注册的完整流程。 +::: + +# 代码生成引擎 + +`src/modules/generator/code_generator.py` 是代码生成器的执行层,负责接收配置字典、渲染模板、写入文件、自动注册路由和菜单。 + +## 核心类:CodeGenerator + +```python +from modules.generator.code_generator import CodeGenerator + +generator = CodeGenerator(config) +generator.generate(auto=True) +``` + +### 构造参数 + +| 参数 | 说明 | +|:----|:----| +| `config` | 配置字典(由 `parser.parse()` 或 JSON 文件提供) | + +构造时自动: +- 调用 `normalize_config()` 标准化配置(幂等,已标准化的配置原样返回) +- 设置输出目录:`src/modules/{app_name}` +- 初始化 Jinja2 模板环境(`trim_blocks=True`, `lstrip_blocks=True`) + +## generate() 主方法 + +```python +def generate(self, auto=False, request=None): +``` + +### 执行流程 + +``` +1. 校验输出目录不存在(避免覆盖已有模块) +2. 创建输出目录 src/modules/{app_name}/ +3. 准备模板上下文数据 +4. 渲染后端模板 → 写入 modules 目录 +5. 渲染 endpoint 模板 → 写入 api/v1/endpoints/ 目录 +6. 渲染前端模板 → 写入 ui/src/views/ 和 ui/src/api/ +7. 自动注册到 router.py +8. 自动创建菜单 + 权限节点 +``` + +### 参数 + +| 参数 | 说明 | +|:----|:----| +| `auto` | 自动模式(当前无交互差异) | +| `request` | Tornado 请求对象(用于获取操作人信息) | + +## 配置标准化 + +`normalize_config()` 将 parser 产出的配置转为模板可用的标准格式: + +```python +# parser 产出的字段 +{"name": "type", "db_type": "Integer", "comment": "案例类型:1-类型1 2-类型2"} + +# 标准化后 +{ + "name": "type", + "camel_name": "type", # 驼峰命名 + "db_type": "Integer", + "form_type": "int", # Pydantic 类型 + "comment": "案例类型:1-类型1 2-类型2", + "py_comment": "案例类型:1-类型1 2-类型2", # 转义后 + "default_repr": "1", # 默认值表示 + "is_status": False, # 状态字段标识 + "is_sort": False, # 排序字段标识 +} +``` + +## 模板上下文 + +### _prepare_context() + +准备后端模板的渲染上下文: + +| 变量 | 类型 | 说明 | +|:----|:----|:----| +| `app_name` | str | 模块名(如 `example`) | +| `module_comment` | str | 中文名(如 `案例`) | +| `model_class_name` | str | 类名(如 `Example`) | +| `permission_prefix` | str | 权限前缀(如 `sys:example`) | +| `display_field` | str | 显示字段(如 `name`) | +| `has_sort` | bool | 有排序字段 | +| `has_status` | bool | 有状态字段 | +| `has_status_route` | bool | 生成状态路由 | +| `has_image_field` | bool | 有图片字段 | +| `has_rich_text_field` | bool | 有富文本字段 | +| `serialize_maps` | dict | 枚举映射 | +| `sort_field` | str | 排序字段名 | +| `fields` | list | 全部字段 | +| `form_fields` | list | 表单字段 | +| `filter_fields` | list | 精确匹配字段 | +| `like_fields` | list | 模糊查询字段 | + +### _prepare_vue_context() + +在后端上下文基础上扩展前端专用变量: + +| 变量 | 类型 | 说明 | +|:----|:----|:----| +| `module_name` | str | 驼峰模块名 | +| `model_class_name_camel` | str | 驼峰类名(首字母小写) | +| `list_fields` | list | 列表展示字段 | +| `searchable_fields` | list | 可搜索字段 | +| `editable_fields` | list | 可编辑字段 | +| `has_export` | bool | 有导出功能 | +| `route_prefix` | str | 路由前缀 | +| `api_path` | str | API 路径 | +| `is_tree_structure` | bool | 是否树形结构 | +| `parent_id_field` | str | 父级字段名 | +| `has_search_form` | bool | 显示搜索表单 | +| `show_selection` | bool | 显示复选框 | + +## 文件生成 + +### 后端文件 + +| 模板 | 输出路径 | 说明 | +|:----|:----|:----| +| `__init__.py.tpl` | `src/modules/{name}/__init__.py` | 模块初始化 | +| `models.py.tpl` | `src/modules/{name}/models.py` | ORM 模型 | +| `schemas.py.tpl` | `src/modules/{name}/schemas.py` | Pydantic 表单 | +| `repository.py.tpl` | `src/modules/{name}/repository.py` | 数据访问层 | +| `service.py.tpl` | `src/modules/{name}/service.py` | 业务逻辑层 | +| `endpoint.py.tpl` | `src/modules/{name}.py` | HTTP 端点 | + +### 前端文件(普通列表) + +| 模板 | 输出路径 | +|:----|:----| +| `ui/index.vue.tpl` | `ui/src/views/tool/{name}/index.vue` | +| `ui/edit.vue.tpl` | `ui/src/views/tool/{name}/edit.vue` | +| `ui/detail.vue.tpl` | `ui/src/views/tool/{name}/detail.vue` | +| `ui/columns.ts.tpl` | `ui/src/views/tool/{name}/columns.ts` | +| `ui/querySchemas.ts.tpl` | `ui/src/views/tool/{name}/querySchemas.ts` | +| `ui/api.ts.tpl` | `ui/src/api/tool/{name}.ts` | + +### 前端文件(树状列表) + +| 模板 | 输出路径 | +|:----|:----| +| `ui2/index.vue.tpl` | `ui/src/views/tool/{name}/index.vue` | +| `ui2/edit.vue.tpl` | `ui/src/views/tool/{name}/edit.vue` | +| `ui2/detail.vue.tpl` | `ui/src/views/tool/{name}/detail.vue` | +| `ui/api.ts.tpl` | `ui/src/api/tool/{name}.ts` | + +树状列表与普通列表共用 `api.ts.tpl`。 + +## 自动注册 + +### 路由注册 + +`_register_module()` 在 `src/api/v1/router.py` 中追加: + +```python +# 导入语句(插入到最后一个 from api.v1.endpoints 导入之后) +from api.v1.endpoints.example import router as example_router + +# 注册语句(追加到文件末尾) +v1._RESOURCE_MODULES 追加(example_router, prefix="/example", tags=["案例"]) +``` + +幂等操作:如果导入语句已存在,跳过。 + +### 菜单创建 + +`_create_menus()` 在数据库中创建: + +1. **二级菜单**(type=0):页面路由 + - name: 模块中文名 + - path: `/tool/{module_camel}` + - component: `/tool/{module_camel}/index` + - parent_id: 189(开发工具) + +2. **三级权限节点**(type=1)× 10 个: + +| 权限 | 标识 | +|:----|:----| +| 分页查询 | `{prefix}:page` | +| 列表查询 | `{prefix}:list` | +| 详情查询 | `{prefix}:detail` | +| 新增 | `{prefix}:add` | +| 编辑 | `{prefix}:update` | +| 删除 | `{prefix}:delete` | +| 批量删除 | `{prefix}:batchDelete` | +| 状态管理 | `{prefix}:status` | +| 导入 | `{prefix}:import` | +| 导出 | `{prefix}:export` | + +幂等操作:如果 path 已存在,跳过。创建后自动清除所有用户权限缓存。 + +## 总结 + +代码生成引擎根据数据库表结构和配置信息,通过 Jinja2 模板渲染生成后端(models/schemas/repository/service/endpoint)和前端(index.vue/edit.vue/columns.ts/querySchemas.ts/api.ts)全套代码,生成后自动注册路由和权限。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume5/config.md b/DjangoAdmin_Tornado_EleVue/zh/volume5/config.md new file mode 100644 index 0000000..505c1a4 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume5/config.md @@ -0,0 +1,317 @@ +:::tip 本章概要 +代码生成器 JSON 配置文件的格式说明,包括模块名称、字段属性、字典编码等配置项的详细解释。 +::: + +# 配置文件详解 + +代码生成器支持通过 JSON 配置文件驱动生成。配置文件可通过 CLI `--output` 导出,也可手动编写,然后用 `--config` 加载生成。 + +## 导出配置 + +```bash +# 从数据库表解析并导出 +python generator.py tornado_example --dry-run --output config.json + +# 生成代码的同时保存配置 +python generator.py tornado_example --output config.json +``` + +## 使用配置生成 + +```bash +python generator.py --config config.json +``` + +跳过表解析,直接从配置文件读取生成参数。适用于: +- 微调字段配置后重新生成 +- 表结构已不存在但配置保留 +- 跨项目复用配置 + +## 配置文件结构 + +```json +{ + "app_name": "example", + "module_comment": "案例", + "model_class_name": "Example", + "permission_prefix": "sys:example", + "display_field": "name", + "primary_key": "id", + "has_sort": true, + "has_status": true, + "has_unique_code": true, + "has_export": false, + "has_image_field": false, + "has_rich_text_field": true, + "fields": [...] +} +``` + +## 顶层配置项 + +| 字段 | 类型 | 必填 | 说明 | +|:----|:----|:----|:----| +| `app_name` | str | 是 | 模块名(小写下划线),用于目录名和文件名 | +| `module_comment` | str | 是 | 模块中文名,用于注释和菜单名 | +| `model_class_name` | str | 是 | 模型类名(帕斯卡命名) | +| `permission_prefix` | str | 否 | 权限前缀,默认 `sys:{app_name}` | +| `display_field` | str | 否 | 显示字段,默认 `name` | +| `primary_key` | str | 否 | 主键字段,默认 `id` | +| `has_sort` | bool | 否 | 有排序字段,默认 `false` | +| `has_status` | bool | 否 | 有状态字段,默认 `false` | +| `has_unique_code` | bool | 否 | 有唯一编码字段,默认 `false` | +| `has_export` | bool | 否 | 有导出功能,默认 `false` | +| `has_image_field` | bool | 否 | 有图片字段,默认 `false` | +| `has_rich_text_field` | bool | 否 | 有富文本字段,默认 `false` | +| `fields` | list | 是 | 字段配置列表 | + +## 字段配置项 + +每个字段对象包含: + +### 基础属性 + +| 字段 | 类型 | 必填 | 说明 | +|:----|:----|:----|:----| +| `name` | str | 是 | 字段名(snake_case) | +| `comment` | str | 否 | 字段注释 | +| `db_type` | str | 是 | SQLAlchemy 类型 | +| `db_args` | str | 否 | 类型参数(如 `"100"`) | +| `form_type` | str | 否 | Pydantic 类型(str/int/float/bool) | +| `max_length` | int | 否 | 最大长度 | + +### 约束属性 + +| 字段 | 类型 | 默认值 | 说明 | +|:----|:----|:----|:----| +| `nullable` | bool | `false` | 是否可空 | +| `required` | bool | `true` | 是否必填(与 nullable 相反) | +| `default` | any | `null` | 默认值 | + +### 展示控制 + +| 字段 | 类型 | 默认值 | 说明 | +|:----|:----|:----|:----| +| `in_form` | bool | `true` | 出现在表单中 | +| `in_list` | bool | `true` | 出现在列表中 | +| `editable` | bool | `true` | 可编辑 | +| `filterable` | bool | `false` | 作为查询条件 | +| `filter_type` | str | `模糊查询` | 筛选类型(`精确匹配`/`模糊查询`) | +| `searchable` | bool | `false` | 可搜索 | + +### 功能标识 + +| 字段 | 类型 | 默认值 | 说明 | +|:----|:----|:----|:----| +| `is_image` | bool | `false` | 图片字段 | +| `is_rich_text` | bool | `false` | 富文本字段 | +| `is_status` | bool | `false` | 状态字段 | +| `is_sort` | bool | `false` | 排序字段 | + +### 选项属性 + +| 字段 | 类型 | 说明 | +|:----|:----|:----| +| `choices` | list | `[[value, label], ...]` 元组列表 | +| `dict_code` | str | 数据字典编码 | +| `min_value` | int | 最小值 | +| `max_value` | int | 最大值 | + +## 完整示例 + +```json +{ + "app_name": "example", + "module_comment": "案例", + "model_class_name": "Example", + "permission_prefix": "sys:example", + "display_field": "name", + "has_sort": true, + "has_status": true, + "has_unique_code": true, + "has_image_field": true, + "has_rich_text_field": true, + "fields": [ + { + "name": "name", + "comment": "案例名称", + "db_type": "String", + "db_args": "100", + "form_type": "str", + "max_length": 100, + "nullable": false, + "required": true, + "in_form": true, + "in_list": true, + "filterable": true, + "filter_type": "模糊查询", + "searchable": true + }, + { + "name": "code", + "comment": "案例编码", + "db_type": "String", + "db_args": "50", + "form_type": "str", + "max_length": 50, + "nullable": false, + "required": true, + "in_form": true, + "in_list": true, + "filterable": true, + "filter_type": "精确匹配", + "searchable": true + }, + { + "name": "type", + "comment": "案例类型:1-类型1 2-类型2 3-类型3", + "db_type": "Integer", + "form_type": "int", + "nullable": false, + "required": true, + "in_form": true, + "in_list": true, + "filterable": true, + "filter_type": "精确匹配", + "choices": [[1, "类型1"], [2, "类型2"], [3, "类型3"]], + "dict_code": "example_type" + }, + { + "name": "status", + "comment": "案例状态:1-正常 2-停用", + "db_type": "Integer", + "form_type": "int", + "default": 1, + "nullable": false, + "required": true, + "in_form": true, + "in_list": true, + "filterable": true, + "filter_type": "精确匹配", + "is_status": true, + "choices": [[1, "正常"], [2, "停用"]], + "dict_code": "example_status" + }, + { + "name": "sort", + "comment": "排序", + "db_type": "Integer", + "form_type": "int", + "default": 0, + "nullable": false, + "required": true, + "in_form": true, + "in_list": true, + "is_sort": true + }, + { + "name": "cover", + "comment": "封面图", + "db_type": "String", + "db_args": "255", + "form_type": "str", + "max_length": 255, + "nullable": true, + "required": false, + "in_form": true, + "in_list": true, + "is_image": true + }, + { + "name": "content", + "comment": "案例内容", + "db_type": "Text", + "form_type": "str", + "nullable": true, + "required": false, + "in_form": true, + "in_list": false, + "is_rich_text": true + }, + { + "name": "remark", + "comment": "备注", + "db_type": "String", + "db_args": "500", + "form_type": "str", + "max_length": 500, + "nullable": true, + "required": false, + "in_form": true, + "in_list": true, + "filterable": true, + "filter_type": "模糊查询" + } + ] +} +``` + +## 字段配置校验规则 + +`db_type` 必须是以下值之一: + +| 合法值 | 对应 SQLAlchemy 类型 | +|:----|:----| +| `String` | String(需配合 `db_args` 指定长度) | +| `Text` | Text | +| `Integer` | Integer | +| `BigInteger` | BigInteger | +| `DateTime` | DateTime | +| `Date` | Date | +| `Time` | Time | +| `Float` | Float | +| `Numeric` | Numeric | +| `Boolean` | Boolean | +| `JSON` | JSON | + +`form_type` 必须是以下值之一:`str` / `int` / `float` / `bool` + +`filter_type` 必须是以下值之一:`精确匹配` / `模糊查询` + +## 常用微调场景 + +### 修改模块中文名 + +```json +{ + "module_comment": "培训记录" +} +``` + +### 隐藏字段(不出现在列表/表单) + +```json +{ + "name": "internal_code", + "in_form": false, + "in_list": false +} +``` + +### 添加自定义字段 + +在 `fields` 数组中追加新的字段配置。 + +### 修改筛选类型 + +```json +{ + "name": "name", + "filterable": true, + "filter_type": "精确匹配" +} +``` + +### 添加选项 + +```json +{ + "name": "level", + "choices": [[1, "初级"], [2, "中级"], [3, "高级"]], + "dict_code": "example_level" +} +``` + +## 总结 + +配置文件为 JSON 格式,包含模块名称、表名、字段属性、字典编码等信息。支持字段级别的精细配置(是否可搜索、是否在列表显示、字典编码映射等),可通过 Web UI 可视化编辑或手动修改 JSON 文件。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume5/demo.md b/DjangoAdmin_Tornado_EleVue/zh/volume5/demo.md new file mode 100644 index 0000000..7e99f7f --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume5/demo.md @@ -0,0 +1,358 @@ +:::tip 本章概要 +以 tornado_example 演示表为完整案例,演示从建表到生成到使用的全流程。 +::: + +# 实战案例 + +以 `tornado_example` 演示表为完整案例,演示从建表到生成到使用的全流程。 + +## 第 1 步:创建数据表 + +在数据库中执行建表语句: + +```sql +CREATE TABLE `tornado_example` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` varchar(100) NOT NULL COMMENT '案例名称', + `code` varchar(50) NOT NULL COMMENT '案例编码', + `type` int DEFAULT 1 COMMENT '案例类型:1-类型1 2-类型2 3-类型3', + `status` int DEFAULT 1 COMMENT '案例状态:1-正常 2-停用', + `sort` int DEFAULT 0 COMMENT '排序', + `cover` varchar(255) DEFAULT NULL COMMENT '封面图', + `content` text COMMENT '案例内容', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + `create_user` varchar(50) DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_user` varchar(50) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `is_delete` int DEFAULT 0, + PRIMARY KEY (`id`) +) COMMENT='案例表'; +``` + +## 第 2 步:预览配置 + +```bash +python generator.py tornado_example --dry-run +``` + +输出: + +``` +============================================================== +数据库表结构解析和代码生成工具 +============================================================== +正在解析表: tornado_example +表结构解析成功 + +------------------------------------------------------------ +生成配置摘要: +------------------------------------------------------------ + 应用名称: example + 模块名称: 案例 + 模型类名: Example + 字段数量: 8 + 是否有排序: True + 是否有状态: True + - name: 案例名称 (String) + - code: 案例编码 (String) + - type: 案例类型 (Integer) + 选项: [(1, '类型1'), (2, '类型2'), (3, '类型3')] + - status: 案例状态 (Integer) + 选项: [(1, '正常'), (2, '停用')] + - sort: 排序 (Integer) + ... 还有 3 个字段 + +============================================================== +预览模式 - 不执行代码生成(去掉 --dry-run 参数即可生成) +============================================================== +``` + +## 第 3 步:生成代码 + +```bash +python generator.py tornado_example +``` + +输出: + +``` +============================================================== +数据库表结构解析和代码生成工具 +============================================================== +正在解析表: tornado_example +表结构解析成功 +... +创建输出目录: src/modules/example +生成文件: src/modules/example/__init__.py +生成文件: src/modules/example/models.py +生成文件: src/modules/example/schemas.py +生成文件: src/modules/example/repository.py +生成文件: src/modules/example/service.py +生成文件: src/modules/example.py +生成文件: ui/src/views/tool/example/index.vue +生成文件: ui/src/views/tool/example/edit.vue +生成文件: ui/src/views/tool/example/detail.vue +生成文件: ui/src/views/tool/example/querySchemas.ts +生成文件: ui/src/views/tool/example/columns.ts +生成文件: ui/src/api/tool/example.ts +已更新路由注册: src/api/v1/router.py +已自动创建菜单: 案例(10 个权限节点) +代码生成完成! +``` + +## 第 4 步:验证生成结果 + +### 检查后端文件 + +```bash +ls src/modules/example/ +# __init__.py models.py repository.py schemas.py service.py + +ls src/modules/example.py +# example.py +``` + +### 检查前端文件 + +```bash +ls ui/src/views/tool/example/ +# index.vue edit.vue detail.vue columns.ts querySchemas.ts + +ls ui/src/api/tool/example.ts +# example.ts +``` + +### 检查路由注册 + +在 `src/api/v1/router.py` 中应新增: + +```python +from api.v1.endpoints.example import router as example_router +... +v1._RESOURCE_MODULES 追加(example_router, prefix="/example", tags=["案例"]) +``` + +## 第 5 步:重启后端 + +```bash +python app.py +``` + +## 第 6 步:访问页面 + +1. 登录管理后台 +2. 左侧菜单找到「开发工具 → 案例」 +3. 进入案例管理页面,测试增删改查功能 + +![](/volume5/4.png) + +## 生成代码逐文件解析 + +### models.py + +```python +class Example(BaseModel): + __tablename__ = DB_PREFIX + "example" + + name = Column(String(100), nullable=False, comment="案例名称") + code = Column(String(50), nullable=False, comment="案例编码") + type = Column(Integer, default=1, comment="案例类型:1-类型1 2-类型2 3-类型3") + status = Column(Integer, default=1, comment="案例状态:1-正常 2-停用") + sort = Column(Integer, default=0, comment="排序") + cover = Column(String(255), nullable=True, comment="封面图") + content = Column(Text, nullable=True, comment="案例内容") + remark = Column(String(500), nullable=True, comment="备注") +``` + +- 自动映射字段类型(varchar→String, int→Integer, text→Text) +- 自动识别 `cover` 为图片字段 +- 自动识别 `content` 为富文本字段(注释含"内容"关键词) +- 系统字段(id/create_user/create_time 等)由基类提供,不重复定义 + +### schemas.py + +```python +class ExampleForm(BaseModel): + id: Optional[int] = Field(None, description="案例ID") + name: str = Field(..., min_length=1, max_length=100, description="案例名称") + code: str = Field(..., min_length=1, max_length=50, description="案例编码") + type: int = Field(..., description="案例类型:1-类型1 2-类型2 3-类型3") + status: int = Field(..., description="案例状态:1-正常 2-停用") + sort: int = Field(..., description="排序") + cover: Optional[str] = Field(None, max_length=255, description="封面图") + content: Optional[str] = Field(None, description="案例内容") + remark: Optional[str] = Field(None, max_length=500, description="备注") + +class ExampleStatusForm(BaseModel): + id: int = Field(..., gt=0, description="案例ID") + status: int = Field(..., ge=1, le=2, description="案例状态:1-正常 2-停用") +``` + +- `name`/`code`/`type`/`status`/`sort` 为必填(`...`) +- `cover`/`content`/`remark` 为可选(`Optional`) +- 自动生成 `StatusForm`(因为有 `status` 字段) + +### service.py + +```python +class ExampleService(BaseService(Example]): + repo = example_repo + model = Example + page_like_fields = ('name', 'code', 'remark') + page_eq_fields = ('type', 'status') + page_order_by = (('sort', 'asc'), ('id', 'desc')) + file_fields = ('cover',) + serialize_maps = {'type': 'example_type', 'status': 'example_status'} + +example_service = ExampleService() +``` + +- `name`/`code`/`remark` 为模糊查询字段(字符串类型) +- `type`/`status` 为精确匹配字段(有选项) +- 默认按 `sort` 升序、`id` 降序排列 +- `cover` 标记为文件字段(自动处理上传和 URL) +- `serialize_maps` 自动关联数据字典 + +### handlers.py + +```python +class ExamplePageHandler(BaseHandler): + @permission_required("sys:example:page") + async def get(self): + return await example_service.get_page(self) + +class ExampleAddHandler(BaseHandler): + @permission_required("sys:example:add") + @check_demo + async def post(self): + return await example_service.add(self) + +# ... update / delete / batchDelete / status / detail / list +``` + +- 自动生成 8 个 Handler(page/detail/list/add/update/delete/batchDelete/status) +- 写操作自动加 `@check_demo` 装饰器 + +## 批量生成示例 + +创建第二张表: + +```sql +CREATE TABLE `tornado_product` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` varchar(200) NOT NULL COMMENT '商品名称', + `price` decimal(10,2) DEFAULT 0 COMMENT '价格', + `stock` int DEFAULT 0 COMMENT '库存', + `status` int DEFAULT 1 COMMENT '状态:1-上架 2-下架', + `sort` int DEFAULT 0 COMMENT '排序', + `create_user` varchar(50) DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_user` varchar(50) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `is_delete` int DEFAULT 0, + PRIMARY KEY (`id`) +) COMMENT='商品表'; +``` + +Web 界面批量生成: + +![](/volume5/3.png) + +```json +{ + "tableNames": [ + "tornado_example|案例表", + "tornado_product|商品表" + ] +} +``` + +CLI 循环生成: + +```bash +python generator.py tornado_example +python generator.py tornado_product +``` + +## 树状表示例 + +当表中包含 `parent_id` 或 `pid` 字段时,生成器自动切换为树状模板。 + +### 创建树状表 + +```sql +CREATE TABLE `tornado_category` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` varchar(100) NOT NULL COMMENT '分类名称', + `parent_id` int DEFAULT 0 COMMENT '父级ID(0为顶级)', + `sort` int DEFAULT 0 COMMENT '排序', + `status` int DEFAULT 1 COMMENT '状态:1-正常 2-停用', + `create_user` varchar(50) DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_user` varchar(50) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `is_delete` int DEFAULT 0, + PRIMARY KEY (`id`) +) COMMENT='分类表'; +``` + +### 生成结果差异 + +因为表中包含 `parent_id` 字段,生成器自动: + +1. **前端模板切换**:使用 `ui2/` 树形模板替代 `ui/` 普通列表模板 +2. **生成文件减少**:不生成 `columns.ts` 和 `querySchemas.ts`(树形组件内联列定义) +3. **数据加载方式**:全量加载 + 前端 `buildTree` 构建树形结构 +4. **交互差异**:支持展开/折叠、父子级联选择 + +``` +普通列表(ui/) 树状列表(ui2/) +├── index.vue ├── index.vue(内联列 + buildTree) +├── edit.vue ├── edit.vue(含父级选择) +├── detail.vue ├── detail.vue +├── columns.ts └── (无 columns.ts) +├── querySchemas.ts └── (无 querySchemas.ts) +└── api.ts └── api.ts(共用) +``` + +## 生成后的自定义 + +代码生成后,通常需要根据业务需求进行微调: + +### 常见自定义项 + +| 自定义项 | 涉及文件 | 说明 | +|:----|:----|:----| +| 添加业务校验 | `schemas.py` | 添加 `@field_validator` 自定义校验逻辑 | +| 添加删除前检查 | `service.py` | 覆盖 `_before_delete` 钩子 | +| 添加创建前处理 | `service.py` | 覆盖 `_before_add` 钩子(如密码哈希) | +| 修改序列化 | `service.py` | 覆盖 `_serialize` 钩子(如关联查询) | +| 添加自定义接口 | `endpoint.py` | 新增路由方法 | +| 调整前端布局 | `index.vue` | 修改表格列宽、操作按钮等 | +| 调整表单校验 | `edit.vue` | 修改表单规则、默认值等 | + +### 示例:添加删除前检查 + +```python +# src/modules/example/service.py +class ExampleService(BaseService(Example]): + # ... 原有配置 ... + + def _before_delete(self, ids) -> Optional[str]: + """删除前校验:检查是否有关联数据""" + # 示例:检查是否有子分类 + id_list = parse_id_list(str(ids)) + if id_list and self.repo.filter( + Example.parent_id.in_(id_list), Example.is_delete == 0 + ).first(): + return "存在子分类,请先删除子分类" + return None +``` + +## 总结 + +通过以上 6 步即可完成一个完整业务模块的生成: +1. 建表 → 2. 预览 → 3. 生成 → 4. 验证 → 5. 重启 → 6. 使用 + +整个过程约 1 分钟,生成约 12 个文件、500+ 行代码,涵盖后端三层 + 前端页面 + 路由注册 + 菜单权限。生成后根据业务需求进行微调即可投入使用。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume5/faq.md b/DjangoAdmin_Tornado_EleVue/zh/volume5/faq.md new file mode 100644 index 0000000..58bca09 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume5/faq.md @@ -0,0 +1,160 @@ +:::tip 本章概要 +代码生成器使用过程中常见问题的原因分析和解决方案。 +::: + +# 常见问题 + +## 生成报错 "模块 xxx 已存在" + +**原因**:生成器会校验目标目录不存在,避免覆盖已有模块。 + +**解决方式**: + +```bash +# 1. 删除已有模块目录 +rm -rf src/modules/example + +# 2. 删除已生成的端点文件 +rm src/modules/example.py + +# 3. 从 router.py 中移除对应的导入和注册语句 +# 删除这两行: +# from api.v1.endpoints.example import router as example_router +# v1._RESOURCE_MODULES 追加(example_router, prefix="/example", tags=["案例"]) + +# 4. 删除数据库中对应的菜单记录 +# DELETE FROM tornado_menu WHERE path = '/tool/example'; + +# 5. 重新生成 +python generator.py tornado_example +``` + +## 生成后前端页面不显示 + +**排查步骤**: + +1. **确认菜单已创建**:检查数据库 `tornado_menu` 表中是否有 `path = '/tool/example'` 的记录 +2. **确认路由组件路径**:菜单的 `component` 字段应为 `tool/example/index` +3. **确认权限已分配**:当前用户需要有对应菜单的访问权限 +4. **清除缓存**:退出重新登录,或清除浏览器缓存 +5. **确认后端已重启**:新路由需要重启后端服务才能生效 + +## 如何重新生成? + +完整清理流程: + +```bash +# 1. 删除后端模块 +rm -rf src/modules/example + +# 2. 删除端点 +rm src/modules/example.py + +# 3. 清理 router.py 中的导入和注册 + +# 4. 删除前端文件 +rm -rf ui/src/views/tool/example +rm ui/src/api/tool/example.ts + +# 5. 删除数据库菜单 +# DELETE FROM tornado_menu WHERE path = '/tool/example'; + +# 6. 重新生成 +python generator.py tornado_example +``` + +## 批量生成有数量限制吗? + +Web API 批量生成最多支持 50 个表(`MAX_BATCH_SIZE = 50`)。CLI 无限制,但建议逐表生成,便于排查问题。 + +## 树状列表是如何判断的? + +当表中存在 `parent_id` 或 `pid` 字段时,自动切换为树状模板(`ui2/`),生成树形结构的增删改查页面。 + +树状列表与普通分页列表的区别: + +| 维度 | 普通列表(ui/) | 树状列表(ui2/) | +|:----|:----|:----| +| 触发条件 | 无 parent_id/pid | 有 parent_id/pid | +| 模板数量 | 5 个组件 + 1 个 API | 3 个组件 + 1 个 API | +| 数据加载 | 分页请求 | 全量加载 + 前端 buildTree | +| 展开/折叠 | 无 | 支持 | +| 父子关系 | 无 | 自动识别 | + +## CLI 报错 "No module named 'core'" + +**原因**:未在项目根目录执行命令。 + +**解决**:确保在项目根目录(包含 `src/` 和 `generator.py` 的目录)执行: + +```bash +cd /data/apps/djangoadmin +python generator.py tornado_example +``` + +## 生成的代码有语法错误 + +**可能原因**: +- 表名或字段名包含特殊字符 +- 字段注释包含未转义的引号 +- 数据库连接异常导致解析不完整 + +**解决**: +1. 使用 `--dry-run` 检查解析结果 +2. 导出配置文件检查字段内容:`python generator.py tornado_example --dry-run --output config.json` +3. 手动修正配置文件后用 `--config` 重新生成 + +## 如何自定义生成的代码风格? + +修改 `src/modules/generator/templates/` 下的模板文件。模板使用 Jinja2 语法,详见 [6.6 自定义模板](/zh/volume5/templates)。 + +修改模板后重新生成即可生效,不影响已有模块。 + +## 生成速度慢 + +**可能原因**: +- 数据库连接延迟高 +- 表字段数量非常多(50+ 字段) +- 同时生成大量表(批量生成) + +**解决**: +- 检查数据库网络连接 +- 减少单次批量生成的表数量 +- 使用 CLI 单表生成替代批量生成 + +## 生成器支持哪些数据库? + +:::warning 数据库限制 +代码生成器通过查询 MySQL 的 `information_schema` 获取表结构信息,**仅支持 MySQL 数据库**。`service.py` 中的 `_query_tables`、`_query_table_detail`、`_query_table_columns` 等函数使用了 MySQL 特有的 SQL 语法(如 `TABLE_SCHEMA = (SELECT DATABASE())`),在其他数据库中可能无法正常执行。 +::: + +代码生成器通过 `information_schema` 查询表元数据,支持以下数据库: + +| 数据库 | 支持程度 | +|:----|:----| +| MySQL | 完全支持(主要开发和测试环境) | +| PostgreSQL | 支持(information_schema 兼容) | +| SQL Server | 支持(information_schema 兼容) | +| SQLite | 部分支持(information_schema 有限) | +| Oracle | 部分支持(需适配) | + +## 模板语法错误如何排查? + +**症状**:生成时报错 `jinja2.exceptions.TemplateSyntaxError`。 + +**排查步骤**: +1. 查看错误信息中的模板文件名和行号 +2. 打开对应的 `.tpl` 文件检查语法 +3. 常见错误: + - `{% if %}` 缺少 `{% endif %}` + - 变量名拼写错误(如 `f.nmae` 应为 `f.name`) + - 使用了未定义的变量(如字段对象中不存在的属性) +4. 修改模板后重新生成 + +## 总结 + +大部分问题可通过以下流程解决: +1. 使用 `--dry-run` 预览配置 +2. 清理旧文件后重新生成 +3. 确认数据库菜单和权限配置正确 +4. 重启后端服务 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume5/overview.md b/DjangoAdmin_Tornado_EleVue/zh/volume5/overview.md new file mode 100644 index 0000000..df9de4f --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume5/overview.md @@ -0,0 +1,141 @@ +:::tip 本章概要 +代码生成器的整体架构、核心模块和生成流程概述。 +::: + +# 概述与架构 + +代码生成器是项目的核心效率工具,能够根据数据库表结构自动生成完整的后端三层模块代码和前端 Vue 页面代码,并自动注册路由和菜单权限节点。 + +## 生成能力总览 + +| 维度 | 生成内容 | +|:----|:----| +| 后端模块 | `__init__.py` / `models.py` / `schemas.py` / `repository.py` / `service.py` | +| 后端路由 | `api/v1/endpoints/{module}.py` + 自动注册到 `router.py` | +| 前端页面 | `index.vue` / `edit.vue` / `detail.vue` / `columns.ts` / `querySchemas.ts` / `api.ts` | +| 菜单权限 | 自动创建菜单节点 + 10 个权限节点 | + +## 两种使用方式 + +| 方式 | 入口 | 适用场景 | +|:----|:----|:----| +| CLI 命令行 | `python generator.py <表名>` | 开发阶段,快速生成单个模块 | +| Web 管理界面 | 开发工具 → 代码生成 | 运维阶段,可视化操作,支持批量生成 | + +两种方式共享同一套引擎代码(`src/modules/generator/`),生成结果完全一致。 + +## 文件结构 + +``` +项目根目录 +├── generator.py # CLI 命令行入口 +└── src/modules/generator/ + ├── parser.py # 表结构解析器 + ├── code_generator.py # 代码生成引擎 + ├── service.py # Web API 业务逻辑 + ├── __init__.py + └── templates/ # Jinja2 模板目录 + ├── __init__.py.tpl # 模块初始化 + ├── models.py.tpl # ORM 模型 + ├── schemas.py.tpl # Pydantic 表单 + ├── repository.py.tpl # 数据访问层 + ├── service.py.tpl # 业务逻辑层 + ├── endpoint.py.tpl # HTTP 端点 + ├── ui/ # 普通分页列表模板(6 个) + │ ├── index.vue.tpl + │ ├── edit.vue.tpl + │ ├── detail.vue.tpl + │ ├── columns.ts.tpl + │ ├── querySchemas.ts.tpl + │ └── api.ts.tpl + └── ui2/ # 树状列表模板(3 个) + ├── index.vue.tpl + ├── edit.vue.tpl + └── detail.vue.tpl +``` + +## 核心组件 + +### parser.py — 表结构解析器 + +从 `information_schema` 读取表元数据,输出标准化配置字典。智能识别图片字段、富文本字段、状态字段、排序字段、枚举字段、查询条件等。 + +### code_generator.py — 代码生成引擎 + +接收配置字典,通过 Jinja2 渲染模板,写入文件,并自动注册路由和菜单权限。 + +### service.py — Web API 业务逻辑 + +提供表分页查询、表详情、字段查询、单表/批量生成等服务方法,供 HTTP 端点调用。 + +### templates/ — 模板目录 + +Jinja2 模板文件,分为后端模板(6 个)和前端模板(ui/ 6 个 + ui2/ 3 个)。含 `parent_id` / `pid` 字段的表自动切换为树形模板(ui2/)。 + +## 生成流程 + +``` + ┌─────────────────────────────────────┐ + │ 数据库 information_schema │ + └──────────────────┬──────────────────┘ + │ + ▼ + ┌─────────────────────────────────────┐ + │ parser.py(表结构解析器) │ + │ ─ 智能识别字段类型/图片/富文本/状态 │ + │ ─ 生成标准化配置字典 │ + └──────────────────┬──────────────────┘ + │ 配置字典 + ▼ + ┌─────────────────────────────────────┐ + │ code_generator.py(代码生成引擎) │ + │ ─ normalize_config() 标准化 │ + │ ─ Jinja2 模板渲染 │ + └───┬──────────┬──────────┬──────────┘ + │ │ │ + ▼ ▼ ▼ + ┌──────────┐ ┌──────────┐ ┌──────────────┐ + │ 后端模块 │ │ 前端页面 │ │ 自动注册 │ + │ models │ │ index.vue│ │ router.py │ + │ schemas │ │ edit.vue │ │ tornado_menu │ + │ repository│ │ columns │ │ 权限节点 │ + │ service │ │ api.ts │ │ 清除缓存 │ + │ endpoint │ │ ... │ │ │ + └──────────┘ └──────────┘ └──────────────┘ +``` + +## 使用限制 + +:::warning 使用前提 +1. **数据库要求**:Web 管理界面的「代码生成」功能基于 MySQL 的 `information_schema` 查询表结构,仅支持 MySQL 数据库。CLI 命令行同样依赖 `information_schema`,需在 MySQL 环境下使用。 +2. **表结构要求**:不支持复合主键、外键关联等复杂表结构,建议使用单主键(自增 id)的标准业务表。 +3. **目录冲突**:生成前会校验目标模块目录不存在,已存在时需先手动删除。 +::: + +## 演示表 + +本章以 `tornado_example` 表为贯穿案例: + +```sql +CREATE TABLE `tornado_example` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` varchar(100) NOT NULL COMMENT '案例名称', + `code` varchar(50) NOT NULL COMMENT '案例编码', + `type` int DEFAULT 1 COMMENT '案例类型:1-类型1 2-类型2 3-类型3', + `status` int DEFAULT 1 COMMENT '案例状态:1-正常 2-停用', + `sort` int DEFAULT 0 COMMENT '排序', + `cover` varchar(255) DEFAULT NULL COMMENT '封面图', + `content` text COMMENT '案例内容', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + `create_user` varchar(50) DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_user` varchar(50) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `is_delete` int DEFAULT 0, + PRIMARY KEY (`id`) +) COMMENT='案例表'; +``` + +## 总结 + +代码生成器通过解析数据库表结构,自动生成符合项目规范的完整模块代码。CLI 适合开发阶段快速生成,Web 界面适合运维阶段批量管理。模板使用 Jinja2 语法,可自由定制生成结果。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume5/parser.md b/DjangoAdmin_Tornado_EleVue/zh/volume5/parser.md new file mode 100644 index 0000000..67d099e --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume5/parser.md @@ -0,0 +1,246 @@ +:::tip 本章概要 +表结构解析引擎的工作原理,从数据库读取表元数据并智能推断字段用途的过程。 +::: + +# 表结构解析引擎 + +`src/modules/generator/parser.py` 是代码生成器的解析层,负责从数据库 `information_schema` 读取表元数据,输出标准化的配置字典。 + +## 核心类:TableSchemaParser + +```python +from modules.generator.parser import TableSchemaParser + +parser = TableSchemaParser("tornado_example") +config = parser.parse() +``` + +### 构造参数 + +| 参数 | 说明 | +|:----|:----| +| `table_name` | 数据库表名,可带前缀(`tornado_example`)或不带前缀(`example`) | + +构造时自动处理: +- 保存原始表名(用于数据库查询) +- 去掉 `tornado_` 前缀(用于生成代码命名) +- 初始化排除字段列表(`id` / `create_user` / `create_time` / `update_user` / `update_time` / `is_delete`) + +## 解析流程 + +`parse()` 方法执行 9 步: + +``` +1. 获取表信息(TABLE_COMMENT、ENGINE 等) +2. 获取字段列表(排除系统字段) +3. 获取索引信息 +4. 清理表名(去掉 tornado_ 前缀) +5. 构建命名(app_name / module_comment / model_class_name) +6. 构建字段配置列表 +7. 判断功能标识(has_sort / has_status / has_unique_code) +8. 获取显示字段(优先 name → title → 第一个字符串字段) +9. 组装完整配置字典 +``` + +## 智能识别规则 + +### 图片字段 + +字段名包含以下关键词时,`is_image` 标记为 `True`: + +``` +avatar / cover / logo / image / photo / picture / img / icon / thumbnail / banner +``` + +效果: +- 前端表单生成图片上传组件 +- 列表中显示图片预览 +- Service 层自动处理文件迁移和 URL 补全 +- 自动设置 `filterable=False`、`searchable=False` + +### 富文本字段 + +`Text` 类型字段且注释包含以下关键词时,`is_rich_text` 标记为 `True`: + +``` +内容 / 介绍 / 说明 / 描述 / 详情 / 正文 +``` + +效果: +- 前端表单生成富文本编辑器 +- Service 层自动进行 XSS 清洗 + +### 状态字段 + +字段名包含以下关键词时,`is_status` 标记为 `True`: + +``` +status / state / is_active / is_enabled / is_deleted +``` + +效果: +- 自动生成状态切换接口(`/status`) +- 前端列表生成状态标签(启用/停用) +- `has_status_route=True` + +### 排序字段 + +字段名包含以下关键词时,`is_sort` 标记为 `True`: + +``` +sort / order / seq / sequence / priority +``` + +效果: +- 分页查询默认按此字段排序 +- `page_order_by` 自动配置 + +### 枚举字段 + +字段注释包含 `数字-文本` 格式(如 `1-正常 2-停用`)时,自动解析为选项列表: + +```python +# 注释:"案例类型:1-类型1 2-类型2 3-类型3" +# 解析结果: +choices = [(1, '类型1'), (2, '类型2'), (3, '类型3')] +dict_code = 'example_type' +``` + +效果: +- 前端表单生成下拉选择框 +- 列表中显示选项文本而非数字 +- 自动设置 `filter_type='精确匹配'` +- 生成数据字典编码,用于 `serialize_maps` + +### 查询条件 + +字段满足以下任一条件时,`filterable` 标记为 `True`: + +1. 字段名包含关键词:`name` / `title` / `code` / `mobile` / `phone` / `email` / `username` / `type` / `category` / `status` / `dept_id` / `role_id` 等 +2. 字段有数据库索引 +3. 整数字段且有选项 + +查询类型判断: +- 字符串字段(非编码类)→ `模糊查询`(LIKE) +- 编码字段(code/sn/no)→ `精确匹配`(=) +- 整数字段 + 有选项 → `精确匹配`(=) +- 状态字段 → `精确匹配`(=) + +## 字段类型映射 + +### 数据库 → SQLAlchemy + +| 数据库类型 | SQLAlchemy 类型 | +|:----|:----| +| varchar / char | String | +| text / longtext / mediumtext / tinytext | Text | +| int / tinyint / smallint / mediumint | Integer | +| bigint | BigInteger | +| datetime / timestamp | DateTime | +| date | Date | +| time | Time | +| float / double / real | Float | +| decimal | Numeric | +| boolean / bit | Boolean | +| json | JSON | + +### SQLAlchemy → Pydantic + +| SQLAlchemy 类型 | Pydantic 类型 | +|:----|:----| +| String / Text | str | +| Integer / BigInteger | int | +| Float / Numeric | float | +| Boolean | bool | +| DateTime / Date / Time | str | +| JSON | str | + +### SQLAlchemy → 中文显示 + +| SQLAlchemy 类型 | 中文 | +|:----|:----| +| String | 字符串 | +| Text | 长文本 | +| Integer | 整数 | +| BigInteger | 大整数 | +| DateTime | 日期时间 | +| Float | 浮点数 | +| Numeric | 小数 | +| Boolean | 布尔值 | + +## 输出配置结构 + +`parse()` 返回的配置字典结构: + +```python +{ + "app_name": "example", # 模块名(去前缀小写) + "module_comment": "案例", # 模块中文名(表注释去掉"表"字) + "model_name": "example", # 模型名 + "model_class_name": "Example", # 模型类名(帕斯卡命名) + "route_prefix": "example", # 路由前缀 + "permission_prefix": "sys:example", # 权限前缀 + "primary_key": "id", # 主键字段 + "display_field": "name", # 显示字段 + "display_field_camel": "name", # 显示字段驼峰 + "table_name": "example", # 表名(去前缀) + "has_sort": True, # 有排序字段 + "has_status": True, # 有状态字段 + "has_unique_code": True, # 有唯一编码字段 + "has_export": False, # 有导出功能 + "has_image_field": False, # 有图片字段 + "has_rich_text_field": True, # 有富文本字段 + "fields": [ # 字段配置列表 + { + "name": "name", + "camel_name": "name", + "comment": "案例名称", + "py_comment": "案例名称", + "db_type": "String", + "db_args": "100", + "form_type": "str", + "type_display": "字符串", + "max_length": 100, + "nullable": False, + "required": True, + "is_unique": False, + "db_index": False, + "default": None, + "default_repr": None, + "in_form": True, + "in_list": True, + "filterable": True, + "filter_type": "模糊查询", + "editable": True, + "searchable": True, + "is_image": False, + "is_rich_text": False, + "is_status": False, + "is_sort": False, + "choices": None, + "min_value": None, + "max_value": None, + }, + # ... 更多字段 + ] +} +``` + +## 与 code_generator.py 的关系 + +``` +parser.py code_generator.py +┌─────────────────────┐ ┌─────────────────────┐ +│ TableSchemaParser │ │ CodeGenerator │ +│ │ │ │ +│ information_schema │ config │ Jinja2 模板渲染 │ +│ ─────────────────> │ ───────> │ ─────────────────> │ +│ 解析表结构 │ 字典 │ 生成文件 │ +└─────────────────────┘ └─────────────────────┘ +``` + +`parser.py` 负责"读",`code_generator.py` 负责"写",通过配置字典解耦。 + +## 总结 + +表结构解析引擎从数据库读取表和字段的元信息(类型、长度、注释、索引等),智能推断字段用途(是否可搜索、是否在列表显示、对应字典编码等),为代码生成器提供结构化的表描述数据。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume5/templates.md b/DjangoAdmin_Tornado_EleVue/zh/volume5/templates.md new file mode 100644 index 0000000..ce41ef9 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume5/templates.md @@ -0,0 +1,296 @@ +:::tip 本章概要 +代码生成器模板的 Jinja2 语法说明,包括模板变量、字段属性和自定义模板的开发方法。 +::: + +# 自定义模板 + +模板文件位于 `src/modules/generator/templates/`,使用 Jinja2 语法。修改模板后重新生成即可生效。 + +## 模板目录结构 + +``` +templates/ +├── __init__.py.tpl # 模块初始化 +├── models.py.tpl # ORM 模型 +├── schemas.py.tpl # Pydantic 表单 +├── repository.py.tpl # 数据访问层 +├── service.py.tpl # 业务逻辑层 +├── endpoint.py.tpl # HTTP 端点 +├── ui/ # 普通分页列表模板 +│ ├── index.vue.tpl # 主页面 +│ ├── edit.vue.tpl # 编辑弹窗 +│ ├── detail.vue.tpl # 详情弹窗 +│ ├── columns.ts.tpl # 表格列定义 +│ ├── querySchemas.ts.tpl # 搜索表单 Schema +│ └── api.ts.tpl # API 请求封装 +└── ui2/ # 树状列表模板 + ├── index.vue.tpl # 树形主页面 + ├── edit.vue.tpl # 树形编辑弹窗 + └── detail.vue.tpl # 树形详情弹窗 +``` + +## ui/ vs ui2/ 切换逻辑 + +当表中存在 `parent_id` 或 `pid` 字段时,自动切换为 `ui2/` 树形模板: + +``` +字段列表中是否有 parent_id / pid? + ├── 是 → 使用 ui2/ 模板(3 个组件 + 1 个 API) + └── 否 → 使用 ui/ 模板(5 个组件 + 1 个 API) +``` + +两者共用 `ui/api.ts.tpl` 生成 API 接口文件。 + +## 模板变量 + +### 基础变量 + +| 变量 | 类型 | 说明 | 示例 | +|:----|:----|:----|:----| +| `app_name` | str | 模块名 | `example` | +| `module_comment` | str | 中文名 | `案例` | +| `module_comment_py` | str | 转义后中文名 | `案例` | +| `model_class_name` | str | 类名 | `Example` | +| `permission_prefix` | str | 权限前缀 | `sys:example` | +| `display_field` | str | 显示字段 | `name` | +| `sort_field` | str | 排序字段 | `sort` | + +### 功能标识 + +| 变量 | 类型 | 说明 | +|:----|:----|:----| +| `has_sort` | bool | 有排序字段 | +| `has_status` | bool | 有状态字段 | +| `has_status_route` | bool | 生成状态路由 | +| `has_image_field` | bool | 有图片字段 | +| `has_rich_text_field` | bool | 有富文本字段 | +| `serialize_maps` | dict | 枚举映射 `{field_name: dict_code}` | + +### 字段列表 + +| 变量 | 类型 | 说明 | +|:----|:----|:----| +| `fields` | list | 全部字段 | +| `form_fields` | list | 表单字段(`in_form=True`) | +| `filter_fields` | list | 精确匹配字段 | +| `like_fields` | list | 模糊查询字段 | + +### 前端专用变量 + +| 变量 | 类型 | 说明 | +|:----|:----|:----| +| `module_name` | str | 驼峰模块名 | +| `model_class_name_camel` | str | 驼峰类名(首字母小写) | +| `list_fields` | list | 列表展示字段 | +| `searchable_fields` | list | 可搜索字段 | +| `editable_fields` | list | 可编辑字段 | +| `is_tree_structure` | bool | 是否树形结构 | +| `parent_id_field` | str | 父级字段名 | +| `route_prefix` | str | 路由前缀 | +| `api_path` | str | API 路径 | +| `has_search_form` | bool | 显示搜索表单 | +| `show_selection` | bool | 显示复选框 | +| `action_column_width` | int | 操作列宽度 | + +## 字段对象属性 + +`fields` 列表中每个字段对象包含以下属性: + +### 基础属性 + +| 属性 | 类型 | 说明 | +|:----|:----|:----| +| `name` | str | 字段名(snake_case) | +| `camel_name` | str | 驼峰字段名 | +| `comment` | str | 字段注释 | +| `py_comment` | str | 转义后注释(可嵌入 Python 字符串) | +| `label` | str | 清洗后的注释(去掉选项说明) | + +### 类型属性 + +| 属性 | 类型 | 说明 | +|:----|:----|:----| +| `db_type` | str | SQLAlchemy 类型 | +| `db_args` | str | 类型参数(如 `"100"`) | +| `form_type` | str | Pydantic 类型 | +| `type_display` | str | 中文类型名 | +| `max_length` | int | 最大长度 | + +### 约束属性 + +| 属性 | 类型 | 说明 | +|:----|:----|:----| +| `nullable` | bool | 是否可空 | +| `required` | bool | 是否必填 | +| `is_unique` | bool | 是否唯一 | +| `default` | any | 默认值 | +| `default_repr` | str | 默认值 Python 表示 | + +### 功能标识 + +| 属性 | 类型 | 说明 | +|:----|:----|:----| +| `is_image` | bool | 图片字段 | +| `is_rich_text` | bool | 富文本字段 | +| `is_status` | bool | 状态字段 | +| `is_sort` | bool | 排序字段 | + +### 展示控制 + +| 属性 | 类型 | 说明 | +|:----|:----|:----| +| `in_form` | bool | 出现在表单中 | +| `in_list` | bool | 出现在列表中 | +| `editable` | bool | 可编辑 | +| `filterable` | bool | 作为查询条件 | +| `filter_type` | str | 筛选类型 | +| `searchable` | bool | 可搜索 | + +### 选项属性 + +| 属性 | 类型 | 说明 | +|:----|:----|:----| +| `choices` | list | `[(value, label), ...]` | +| `dict_code` | str | 数据字典编码 | +| `min_value` | int | 最小值 | +| `max_value` | int | 最大值 | + +### 前端专用 + +| 属性 | 类型 | 说明 | +|:----|:----|:----| +| `purpose` | str | 用途说明 | +| `type` | str | 配置字段类型名(CharField/IntegerField/...) | +| `choices[].value` | any | 选项值 | +| `choices[].label` | str | 选项文本 | +| `choices[].type` | str | 标签颜色(success/danger/warning/info) | + +## 自定义模板示例 + +### 示例 1:为所有字段添加索引 + +修改 `templates/models.py.tpl`: + +```jinja +{% if f.db_type == 'String' %} + {{ f.name }} = Column(String({{ f.db_args or 255 }}), nullable={{ 'True' if f.nullable else 'False' }}, index=True, comment="{{ f.comment }}") +{% endif %} +``` + +### 示例 2:自定义 Service 钩子 + +修改 `templates/service.py.tpl`,在 Service 类中添加钩子方法: + +```jinja +class {{ model_class_name }}Service(BaseService({{ model_class_name }}]): + # ... 差异点声明 ... + +{% if has_status %} + def _before_delete(self, ids) -> Optional[str]: + """删除前校验:检查是否有关联数据""" + # TODO: 添加自定义校验逻辑 + return None +{% endif %} +``` + +### 示例 3:自定义前端表格列宽 + +修改 `templates/ui/columns.ts.tpl`: + +```jinja +{% for f in list_fields %} + { + label: '{{ f.label }}', + prop: '{{ f.camel_name }}', + minWidth: {{ 80 + (f.max_length or 100) // 3 }}, {# 动态列宽 #} + }, +{% endfor %} +``` + +## 新增模板 + +如需生成额外文件(如测试文件),按以下步骤操作: + +### 第 1 步:创建模板文件 + +在 `templates/` 下新建 `test.py.tpl`: + +```jinja +# tests/test_{{ app_name }}.py +import pytest +from modules.{{ app_name }}.service import {{ app_name }}_service + + +class Test{{ model_class_name }}: + def test_get_page(self, client): + response = client.get("/api/v1/{{ app_name }}/page") + assert response.status_code == 200 +``` + +### 第 2 步:注册到 BACKEND_TEMPLATES + +在 `code_generator.py` 中添加: + +```python +BACKEND_TEMPLATES = [ + '__init__.py.tpl', + 'models.py.tpl', + 'schemas.py.tpl', + 'repository.py.tpl', + 'service.py.tpl', + 'test.py.tpl', # 新增 +] +``` + +### 第 3 步:调整输出路径(可选) + +如果需要输出到非默认目录,修改 `generate()` 方法中的路径逻辑。 + +## Jinja2 语法速查 + +```jinja +{# 注释 #} + +{# 变量输出 #} +{{ app_name }} +{{ f.name | upper }} + +{# 条件判断 #} +{% if has_status %} + # 有状态字段 +{% elif has_sort %} + # 有排序字段 +{% else %} + # 都没有 +{% endif %} + +{# 循环 #} +{% for f in fields %} + {{ f.name }} +{% endfor %} + +{# 循环 + 条件过滤 #} +{% for f in fields if f.is_image %} + {{ f.name }} +{% endfor %} + +{# 循环变量 #} +{% for f in fields %} + {{ loop.index }} {# 从 1 开始 #} + {{ loop.first }} {# 是否第一个 #} + {{ loop.last }} {# 是否最后一个 #} +{% endfor %} + +{# 三元表达式 #} +{{ 'True' if f.nullable else 'False' }} + +{# 字符串拼接 #} +{{ app_name }}_repo + +{# 默认值 #} +{{ f.db_args or 255 }} +``` + +## 总结 + +模板使用 Jinja2 语法,每个模板对应一个生成文件。模板变量包括模块名、表名、字段列表等,字段属性包含名称、类型、注释、是否可搜索等。可通过修改模板自定义生成代码的风格和结构。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume5/web-ui.md b/DjangoAdmin_Tornado_EleVue/zh/volume5/web-ui.md new file mode 100644 index 0000000..2a73c41 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume5/web-ui.md @@ -0,0 +1,142 @@ +:::tip 本章概要 +代码生成器 Web 管理界面的使用方法,通过可视化界面操作单表生成和批量生成。 +::: + +# Web 管理界面 + +登录管理后台,进入「开发工具 → 代码生成」页面,可通过可视化界面操作代码生成。 + +![](/demo/27.png) + +:::warning 数据库要求 +代码生成器的 Web 界面通过查询 MySQL 的 `information_schema` 获取表结构信息,**仅支持 MySQL 数据库**。如使用 PostgreSQL / SQL Server / SQLite 等其他数据库,请使用 CLI 命令行方式生成(需自行适配查询语句)。 +::: + +## 操作流程 + +完整操作流程: + +``` +1. 登录管理后台 +2. 左侧菜单 → 开发工具 → 代码生成 +3. 在表列表中搜索目标表(支持表名/表注释模糊搜索) +4. 点击表名可查看字段详情(字段名、类型、注释、是否可空等) +5. 单表生成:点击行末「生成」按钮 + 批量生成:勾选多表 → 点击「批量生成」按钮 +6. 生成成功后,左侧菜单自动出现新模块入口 +7. 进入新模块页面,测试增删改查功能 +``` + +## 单表生成 + +1. 在表列表中搜索目标表(支持表名和表注释模糊搜索) +2. 点击目标表行的「生成」按钮 +3. 系统自动解析表结构并生成全部代码 +4. 生成完成后提示成功 + +![](/volume5/2.png) + +## 批量生成 + +1. 勾选多个表(左侧复选框) +2. 点击表格上方的「批量生成」按钮 +3. 系统逐表生成,完成后提示成功数量 + +![](/volume5/3.png) + +批量生成限制:最多 50 个表(`MAX_BATCH_SIZE = 50`)。 + +## API 接口 + +Web 界面底层调用以下 API 接口: + +### 查询表分页 + +``` +GET /api/v1/generator/page?pageNo=1&pageSize=10&tableName=example +``` + +返回数据库表的分页数据,支持按表名和表注释模糊搜索。 + +### 查询表详情 + +``` +GET /api/v1/generator/detail/{table_name} +``` + +返回单个表的元数据(表名、注释、引擎、行数、字符集等)。 + +### 查询表字段 + +``` +GET /api/v1/generator/columns/{table_name} +``` + +返回表的字段列表(字段名、类型、注释、是否可空、默认值等)。 + +### 代码生成 + +``` +POST /api/v1/generator/generate +``` + +请求体: + +```json +{ + "tableNames": [ + "tornado_example|案例表", + "tornado_product|商品表" + ] +} +``` + +`tableNames` 支持三种格式: + +| 格式 | 示例 | 说明 | +|:----|:----|:----| +| 管道分隔 | `"tornado_example\|案例表"` | 表名\|注释,注释覆盖数据库表注释 | +| 纯表名 | `"tornado_example"` | 注释从数据库读取 | +| 字典 | `{"name": "tornado_example", "comment": "案例表"}` | 结构化格式 | + +成功返回: + +```json +{ + "code": 0, + "msg": "本次共计生成2个模块", + "data": 2 +} +``` + +### 批量生成 + +``` +POST /api/v1/generator/batchGenerate +``` + +请求体和返回格式与「代码生成」接口完全一致。 + +## 权限节点 + +| 权限标识 | 说明 | +|:----|:----| +| `sys:generator:page` | 查询表分页 | +| `sys:generator:detail` | 查询表详情/字段 | +| `sys:generator:generate` | 代码生成 | +| `sys:generator:batchGenerate` | 批量生成 | + +## 与 CLI 的对比 + +| 维度 | CLI | Web 界面 | +|:----|:----|:----| +| 入口 | `python generator.py <表名>` | 开发工具 → 代码生成 | +| 适用场景 | 开发阶段,快速生成 | 运维阶段,可视化操作 | +| 批量生成 | 不支持(循环调用) | 支持(勾选多表) | +| 配置微调 | `--output` 导出 → 编辑 → `--config` 导入 | 不支持(直接生成) | +| 生成结果 | 完全一致 | 完全一致 | +| 预览模式 | `--dry-run` | 不支持 | + +## 总结 + +Web 管理界面提供可视化的代码生成操作,支持单表生成和批量生成。通过选择数据库表、配置生成选项、预览生成文件,一键生成前后端全套代码并自动注册路由和权限。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume6/backup.md b/DjangoAdmin_Tornado_EleVue/zh/volume6/backup.md new file mode 100644 index 0000000..cad5869 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume6/backup.md @@ -0,0 +1,235 @@ +# 备份与恢复 + +## 概述 + +定期备份是防止数据丢失的最后防线。本文介绍 MySQL 数据库备份、Redis 数据备份、上传文件备份的方案,以及自动化备份调度和恢复操作。 + +:::tip 备份策略 3-2-1 原则 +- **3** 份数据副本(1 份生产 + 2 份备份) +- **2** 种不同存储介质(如本地磁盘 + 远程存储) +- **1** 份异地备份(防机房级灾难) +::: + +## MySQL 数据库备份 + +### 全量备份 + +```bash +# 使用 mysqldump 备份单个数据库 +mysqldump -h 127.0.0.1 -u root -p --single-transaction \ + --routines --triggers --events \ + djangoadmin_prod > backup_$(date +%Y%m%d_%H%M%S).sql + +# 压缩备份 +mysqldump -h 127.0.0.1 -u root -p --single-transaction \ + djangoadmin_prod | gzip > backup_$(date +%Y%m%d_%H%M%S).sql.gz +``` + +:::tip --single-transaction 作用 +`--single-transaction` 参数确保 InnoDB 表的备份一致性,不锁表,不影响线上服务。 +::: + +### 增量备份 + +基于 MySQL binlog 实现增量备份: + +```bash +# 查看当前 binlog 位置 +mysql -u root -p -e "SHOW MASTER STATUS;" + +# 备份 binlog +mysqlbinlog --start-datetime="2026-09-05 00:00:00" \ + mysql-bin.000001 > binlog_incremental.sql +``` + +:::tip binlog 配置 +确保 MySQL 配置中启用了 binlog: +```ini +[mysqld] +server-id = 1 +log-bin = mysql-bin +binlog-format = ROW +expire_logs_days = 7 +``` +::: + +### 仅备份表结构 + +```bash +# 仅备份结构(不含数据) +mysqldump -h 127.0.0.1 -u root -p --no-data \ + djangoadmin_prod > schema_$(date +%Y%m%d).sql +``` + +## Redis 数据备份 + +### RDB 快照 + +```bash +# 手动触发 RDB 快照 +redis-cli BGSAVE + +# 查看最后一次快照时间 +redis-cli LASTSAVE + +# 复制 RDB 文件 +cp /var/lib/redis/dump.rdb /data/backup/redis_$(date +%Y%m%d).rdb +``` + +### AOF 持久化 + +```bash +# redis.conf 配置 +appendonly yes +appendfsync everysec + +# 备份 AOF 文件 +cp /var/lib/redis/appendonly.aof /data/backup/redis_$(date +%Y%m%d).aof +``` + +:::tip Redis 备份建议 + Redis 用于缓存和 token 黑名单,数据可从数据库重建。建议使用 RDB 快照即可,AOF 可选。 +::: + +## 上传文件备份 + +```bash +# 打包上传目录 +tar -czf uploads_$(date +%Y%m%d).tar.gz /data/apps/uploads/ + +# 使用 rsync 增量同步到远程 +rsync -avz --delete /data/apps/uploads/ backup@10.0.2.100:/data/backup/uploads/ +``` + +## 自动化备份 + +### 备份脚本 + +创建 `/data/scripts/backup.sh`: + +```bash +#!/bin/bash +# 备份目录 +BACKUP_DIR="/data/backup" +DATE=$(date +%Y%m%d_%H%M%S) +RETENTION_DAYS=7 + +# 创建备份目录 +mkdir -p ${BACKUP_DIR}/{mysql,redis,uploads} + +# MySQL 备份 +mysqldump -h 127.0.0.1 -u djangoadmin -p'<密码>' \ + --single-transaction djangoadmin_prod | gzip > \ + ${BACKUP_DIR}/mysql/backup_${DATE}.sql.gz + +# Redis 备份 +redis-cli BGSAVE +sleep 5 +cp /var/lib/redis/dump.rdb ${BACKUP_DIR}/redis/redis_${DATE}.rdb + +# 上传文件备份 +tar -czf ${BACKUP_DIR}/uploads/uploads_${DATE}.tar.gz /data/apps/uploads/ + +# 清理过期备份 +find ${BACKUP_DIR} -type f -mtime +${RETENTION_DAYS} -delete + +echo "[$(date)] Backup completed" >> /var/log/backup.log +``` + +### Crontab 配置 + +```bash +# 编辑定时任务 +crontab -e + +# 每天凌晨 2 点执行备份 +0 2 * * * /bin/bash /data/scripts/backup.sh >> /var/log/backup.log 2>&1 + +# 每周日凌晨 3 点执行全量备份 +0 3 * * 0 /bin/bash /data/scripts/backup_full.sh >> /var/log/backup.log 2>&1 +``` + +:::tip Crontab 说明 +``` +分 时 日 月 周 命令 +0 2 * * * /bin/bash /data/scripts/backup.sh +│ │ │ │ │ +│ │ │ │ └── 星期几 (0-7, 0和7都是周日) +│ │ │ └──── 月份 (1-12) +│ │ └─────── 日 (1-31) +│ └────────── 时 (0-23) +└───────────── 分 (0-59) +``` +::: + +## 数据恢复 + +### MySQL 恢复 + +```bash +# 从 SQL 文件恢复 +mysql -h 127.0.0.1 -u root -p djangoadmin_prod < backup_20260905.sql + +# 从压缩文件恢复 +gunzip < backup_20260905.sql.gz | mysql -h 127.0.0.1 -u root -p djangoadmin_prod +``` + +### Redis 恢复 + +```bash +# 停止 Redis +redis-cli SHUTDOWN NOSAVE + +# 替换 RDB 文件 +cp /data/backup/redis/redis_20260905.rdb /var/lib/redis/dump.rdb + +# 启动 Redis +redis-server /etc/redis/redis.conf +``` + +### 上传文件恢复 + +```bash +# 解压恢复 +tar -xzf uploads_20260905.tar.gz -C / +``` + +## 恢复演练 + +:::warning 重要 +定期进行恢复演练,验证备份文件的完整性和可用性。建议每季度至少演练一次。 +::: + +```bash +# 恢复演练步骤 +# 1. 在测试环境创建空库 +mysql -u root -p -e "CREATE DATABASE djangoadmin_restore;" + +# 2. 恢复备份 +mysql -u root -p djangoadmin_restore < backup_20260905.sql + +# 3. 验证数据完整性 +mysql -u root -p -e " + SELECT 'user' AS tbl, COUNT(*) AS cnt FROM djangoadmin_restore.tornado_user + UNION ALL + SELECT 'article', COUNT(*) FROM djangoadmin_restore.tornado_article; +" + +# 4. 清理测试库 +mysql -u root -p -e "DROP DATABASE djangoadmin_restore;" +``` + +## 备份监控 + +```bash +# 检查备份文件是否存在且非空 +find /data/backup/mysql -name "*.sql.gz" -mtime -1 -size +0 | wc -l +# 返回 0 则告警 + +# 检查备份文件大小 +ls -lh /data/backup/mysql/backup_$(date +%Y%m%d)*.sql.gz +``` + +## 总结 + +备份方案覆盖 MySQL(mysqldump 全量 + binlog 增量)、Redis(RDB 快照)、上传文件(tar 打包 + rsync 同步)。通过 Crontab 实现自动化调度,保留最近 7 天备份。关键要点:备份文件异地存储、定期恢复演练验证完整性、监控备份任务执行状态。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume6/db-cross-migrate.md b/DjangoAdmin_Tornado_EleVue/zh/volume6/db-cross-migrate.md new file mode 100644 index 0000000..ff88e37 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume6/db-cross-migrate.md @@ -0,0 +1,83 @@ +# 跨库数据迁移 + +:::tip 说明 +项目提供 `scripts/migrate_db.py` 脚本,支持在不同数据库之间迁移数据(如 MySQL → PostgreSQL / SQL Server / SQLite / Oracle)。脚本独立运行,参数显式给出,不依赖 `.env`。 +::: + +## 使用场景 + +| 场景 | 说明 | +|:-----|:-----| +| MySQL → PostgreSQL | 生产环境切换数据库 | +| MySQL → SQL Server | 企业环境适配 | +| MySQL → SQLite | 开发/测试环境轻量化 | +| MySQL → Oracle | 企业级数据库适配 | + +## 命令行参数 + +```bash +python scripts/migrate_db.py --help +``` + +| 参数 | 必填 | 说明 | 示例 | +|:-----|:-----|:-----|:-----| +| `--source-db` | 是 | 源数据库类型 | `mysql` | +| `--target-db` | 是 | 目标数据库类型 | `postgresql` | +| `--source-url` | 是 | 源数据库连接 URL | `mysql+pymysql://root:123456@localhost:3306/db` | +| `--target-url` | 是 | 目标数据库连接 URL | `postgresql://postgres:123456@localhost:5432/db` | + +## 使用示例 + +### MySQL → PostgreSQL + +```bash +python scripts/migrate_db.py \ + --source-db mysql \ + --target-db postgresql \ + --source-url "mysql+pymysql://root:123456@localhost:3306/djangoadmin" \ + --target-url "postgresql://postgres:123456@localhost:5432/djangoadmin" +``` + +### MySQL → SQLite + +```bash +python scripts/migrate_db.py \ + --source-db mysql \ + --target-db sqlite \ + --source-url "mysql+pymysql://root:123456@localhost:3306/djangoadmin" \ + --target-url "sqlite:///djangoadmin.db" +``` + +## 迁移流程 + +``` +1. 连接源数据库和目标数据库 +2. 读取源数据库所有表结构 +3. 在目标数据库创建对应表(Base.metadata.create_all) +4. 逐表读取源数据 → 批量插入目标数据库 +5. 输出迁移统计(表数、行数、耗时) +``` + +## 注意事项 + +:::warning 迁移前备份 +跨库迁移前务必备份目标数据库,避免数据丢失。 +::: + +- 迁移脚本会先在目标库创建表结构(如已存在则跳过) +- 数据迁移采用批量插入,大表迁移可能需要较长时间 +- 不同数据库的字段类型差异由 SQLAlchemy 方言层自动处理 +- 迁移完成后建议在目标库执行 `scripts/seed_dict.py` 补充字典数据 + +## 相关脚本 + +| 脚本 | 用途 | +|:-----|:-----| +| `scripts/migrate_db.py` | 跨库数据迁移 | +| `scripts/seed_dict.py` | 字典数据初始化 | +| `scripts/init_db.py` | 数据库初始化(建库建表) | +| `scripts/health_check.py` | 健康检查 | + +## 总结 + +跨库数据迁移脚本支持 MySQL / PostgreSQL / SQL Server / SQLite / Oracle 之间的数据迁移,参数显式给出,不依赖 `.env` 配置。迁移前务必备份目标数据库。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume6/db-migrate.md b/DjangoAdmin_Tornado_EleVue/zh/volume6/db-migrate.md new file mode 100644 index 0000000..08cbfde --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume6/db-migrate.md @@ -0,0 +1,459 @@ +# 数据库迁移 + +## 概述 + +使用 Alembic 管理数据库结构迁移,同时提供跨库数据迁移脚本,支持从 MySQL 迁移到 PostgreSQL / SQL Server / SQLite / Oracle 等多种数据库。 + +:::tip 迁移工具链 +- **结构迁移**:Alembic(自动生成 DDL 差异) +- **数据迁移**:`scripts/migrate_db.py`(跨库数据泵) +- **初始化**:`scripts/init_db.py`(全新安装一键建库) +::: + +## 全新安装初始化 + +首次部署时,使用初始化脚本一键完成建库、建表、Alembic stamp: + +```bash +# 方式一:直接运行脚本 +python scripts/init_db.py + +# 方式二:Make 命令 +make db-init +``` + +:::tip 初始化流程 +1. 检测并创建数据库(如不存在) +2. 执行 `Base.metadata.create_all()` 建表 +3. 导入基础 SQL 数据(如有) +4. Alembic stamp head(标记当前为最新版本) +::: + +## Alembic 结构迁移 + +### 生成迁移脚本 + +修改模型后,执行以下命令自动生成迁移脚本: + +```bash +# 生成增量迁移脚本 +alembic revision --autogenerate -m "描述变更内容" + +# 或使用 Make 命令 +make migrate-revision +``` + +:::tip 注意事项 +- 执行前确保数据库连接正常(`.env` 中的数据库配置正确) +- 自动生成的脚本需人工审查,确认无误后再执行 +- 迁移脚本位于 `migrations/versions/` 目录 +::: + +### 应用迁移 + +```bash +# 应用所有待执行的迁移 +alembic upgrade head + +# 或使用 Make 命令 +make migrate-upgrade +``` + +### 回滚迁移 + +```bash +# 回滚最近一次迁移 +alembic downgrade -1 + +# 回滚到指定版本 +alembic downgrade +``` + +### 查看迁移状态 + +```bash +# 查看当前版本 +alembic current + +# 查看迁移历史 +alembic history + +# 查看待执行的迁移 +alembic history --indicate-current +``` + +### 手动标记版本 + +对于已有数据库,手动标记为最新版本: + +```bash +alembic stamp head + +# 或使用 Make 命令 +make migrate-stamp +``` + +## 跨库数据迁移 + +将源库(通常 MySQL)的结构和数据整体迁移到目标库(PostgreSQL / SQL Server / SQLite / Oracle)。 + +### 脚本说明 + +`scripts/migrate_db.py` 是项目内置的跨库迁移脚本,核心特性: + +- **目标库结构**:用模型 metadata 生成(`Base.metadata.create_all`),方言可移植,模型即 head +- **数据泵**:按拓扑序逐表 `SELECT ... ORDER BY id`,显式带 id 批量插入,保住无外键裸列引用(`user.dept_id`、`role_menu.role_id/menu_id`、`menu.parent_id` 等) +- **自增序列重置**:迁移后按方言自动处理(详见下文) +- **进度提示**:全部耗时步骤带缓冲提示(`[·]` 请勿中断 → `[✓]` 完成),不会显得卡死 +- **连接独立**:源/目标连接全部由 CLI 显式参数给出,不依赖 `.env`,也无需先切 `DB_DRIVER` + +### 前置准备 + +1. 源库(MySQL)可正常访问 +2. 目标库所需驱动已包含在 `requirements.txt`: + +| 目标库 | 驱动包 | 说明 | +|:----|:----|:----| +| PostgreSQL | psycopg[binary] | psycopg3 | +| SQL Server | pymssql | - | +| SQLite | 内置 | 无需额外驱动 | +| Oracle | oracledb | thin 模式,无需安装 Oracle Client | + +```bash +pip install -r requirements.txt +``` + +### 驱动与默认端口 + +| 目标库 | `--dst-driver` | 默认端口 | `--dst-db` 填什么 | +|:----|:----|:----|:----| +| MySQL | `mysql` | 3306 | 数据库名 | +| PostgreSQL | `postgresql` | 5432 | 数据库名(含点号会自动双引号) | +| SQL Server | `mssql` | 1433 | 数据库名 | +| SQLite | `sqlite` | 无端口 | SQLite 文件路径(相对/绝对皆可) | +| Oracle | `oracle` | 1521 | SERVICE_NAME(如 ORCLPDB1、XE,非库名) | + +驱动别名:`postgres` / `pg` → `postgresql`;`sqlserver` / `sql_server` → `mssql`;`sqlite3` → `sqlite` + +### 迁移流程 + +迁移分 5 个阶段自动执行: + +``` +1. 建目标库(create_database_if_missing) + ├─ MySQL: CREATE DATABASE IF NOT EXISTS + ├─ PG: 连接 postgres 维护库 → CREATE DATABASE + ├─ MSSQL: 连接 master 库 → CREATE DATABASE + ├─ Oracle: 不建库,只清理旧表(schema 即连接用户) + └─ SQLite: 创建文件目录 + 可选删旧文件 + +2. 建表结构(Base.metadata.create_all) + └─ 方言适配,自动生成对应数据库的 DDL + +3. 逐表泵数据(拓扑序,分块 executemany) + └─ 显式带 id 插入,保住引用完整性 + +4. 重置自增序列(按方言处理) + ├─ PostgreSQL: setval(seq, MAX(id)) + ├─ SQL Server: IDENTITY_INSERT ON + DBCC CHECKIDENT RESEED + ├─ Oracle: 补 sequence + BEFORE INSERT 触发器 + └─ MySQL/SQLite: 自动跟随,无需处理 + +5. 逐表 COUNT(*) 校验 + └─ source vs target 行数一致 → PASS +``` + +### 常用参数 + +| 参数 | 说明 | +|:----|:----| +| `--src-driver` / `--src-host` / `--src-port` / `--src-db` / `--src-user` / `--src-pass` | 源库连接参数 | +| `--dst-driver` / `--dst-host` / `--dst-port` / `--dst-db` / `--dst-user` / `--dst-pass` | 目标库连接参数 | +| `--dry-run` | 干跑模式,只连源库逐表数行数,不碰目标库 | +| `--drop-target-first` | 迁移前删除目标库所有表(幂等重跑) | +| `--only-tables a,b` | 仅迁移指定表(续传用) | +| `--skip-existing` | 目标表行数已等于源表则跳过 | +| `--skip-verify` | 跳过迁移后的逐表行数校验 | +| `--chunk-size N` | 每批插入行数,默认 1000 | +| `--single-transaction` | 全部表包进一个大事务(默认每表一个事务) | + +口令脱敏:`--src-pass` / `--dst-pass` 留空时回落环境变量 `SRC_DB_PASS` / `DST_DB_PASS`,避免口令进 shell 历史。 + +### 干跑示例 + +迁移前建议先干跑核对计划: + +```bash +# 干跑:核对源库表和行数 +python scripts/migrate_db.py --dry-run \ + --src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \ + --src-user=root --src-pass=root --src-db=djangoadmin.tornado.elevue2 + +# 或使用 Make 命令 +make db-migrate-dry-run ARGS="\ + --src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \ + --src-user=root --src-pass=root --src-db=djangoadmin.tornado.elevue2" +``` + +### MySQL → PostgreSQL + +```bash +python scripts/migrate_db.py \ + --src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \ + --src-db=djangoadmin.tornado.elevue2 --src-user=root --src-pass=root \ + --dst-driver=postgresql --dst-host=localhost --dst-port=5432 \ + --dst-db=djangoadmin.tornado.elevue2 --dst-user=postgres --dst-pass=你的密码 \ + --drop-target-first +``` + +输出示例: + +``` +============================================================== +[迁移开始] 源: mysql:djangoadmin.tornado.elevue2 → 目标: postgresql:djangoadmin.tornado.elevue2 +[迁移开始] 待迁移 23 张表 +============================================================== +[·] [1/5] 建目标库... + · 正在连接 postgres 维护库... + · 正在创建目标库 "djangoadmin.tornado.elevue2"(UTF8)... +[✓] [1/5] 建目标库 完成(耗时 947ms) +[·] [2/5] 建表结构(23 张表 create_all)... +[✓] [2/5] 建表结构 完成(耗时 1.1s),23 张表 +[·] [3/5] 逐表迁移数据... + · [1/23] 正在迁移 tornado_dept... + [✓ tornado_dept] 共 23 行(耗时 12ms) + · [3/23] 正在迁移 tornado_city... + [tornado_city] +1000(累计 1000) + [tornado_city] +1000(累计 2000) + [tornado_city] +1000(累计 3000) + [tornado_city] +1000(累计 4000) + [tornado_city] +150(累计 4150) + [✓ tornado_city] 共 4150 行(耗时 850ms) + ... +[✓] [3/5] 逐表迁移数据 完成(耗时 3.5s),共 4843 行 +[·] [4/5] 重置自增序列... +[✓] [4/5] 重置自增序列 完成(耗时 412ms) ← PG setval 到 MAX(id) +[·] [5/5] 逐表行数校验... + [tornado_dept] source=23 target=23 PASS + [tornado_city] source=4150 target=4150 PASS + ... +[✓] [5/5] 逐表行数校验 完成(耗时 103ms),23 张表全部 PASS +============================================================== +[迁移完成] 成功:4843 行 / 23 张表 / 总耗时 6.0s +============================================================== +``` + +### MySQL → SQL Server + +```bash +python scripts/migrate_db.py \ + --src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \ + --src-db=djangoadmin.tornado.elevue2 --src-user=root --src-pass=root \ + --dst-driver=mssql --dst-host=localhost --dst-port=1433 \ + --dst-db=djangoadmin.tornado.elevue2 --dst-user=sa --dst-pass=你的密码 \ + --drop-target-first +``` + +:::tip SQL Server 特殊处理 +迁移时自动 `SET IDENTITY_INSERT ON` 保留原 id,迁移后 `DBCC CHECKIDENT RESEED` 重置自增。 +::: + +输出示例: + +``` +============================================================== +[迁移开始] 源: mysql → 目标: mssql:djangoadmin.tornado.elevue2 +[迁移开始] 待迁移 23 张表 +============================================================== +[·] [1/5] 建目标库... + · 正在连接 SQL Server(master 维护库)... + · 正在删除旧目标库 [djangoadmin.tornado.elevue2](SINGLE_USER + DROP)... + · 正在创建目标库 [djangoadmin.tornado.elevue2]... +[✓] [1/5] 建目标库 完成(耗时 304ms) +[·] [2/5] 建表结构(23 张表 create_all)... +[✓] [2/5] 建表结构 完成(耗时 743ms),23 张表 +[·] [3/5] 逐表迁移数据... + ... + [✓ tornado_city] 共 4150 行(耗时 3.9s) + ... +[✓] [3/5] 逐表迁移数据 完成(耗时 5.3s),共 4843 行 +[·] [4/5] 重置自增序列... +[✓] [4/5] 重置自增序列 完成(耗时 90ms) +[·] [5/5] 逐表行数校验... +[✓] [5/5] 逐表行数校验 完成(耗时 100ms),23 张表全部 PASS +============================================================== +[迁移完成] 成功:4843 行 / 23 张表 / 总耗时 6.6s +============================================================== +``` + +### MySQL → SQLite + +```bash +python scripts/migrate_db.py \ + --src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \ + --src-db=djangoadmin.tornado.elevue2 --src-user=root --src-pass=root \ + --dst-driver=sqlite --dst-db=D:/data/djangoadmin.tornado.elevue2.db \ + --drop-target-first +``` + +:::tip SQLite 说明 +- `--dst-db` 即数据库文件完整路径(相对/绝对皆可) +- SQLite 无建库概念,`--drop-target-first` 时删除旧文件后重建 +- 自增 id 由 rowid 自动跟随,无需重置序列 +::: + +输出示例: + +``` +============================================================== +[迁移开始] 源: mysql → 目标: sqlite:D:/data/djangoadmin.tornado.elevue2.db +[迁移开始] 待迁移 23 张表 +============================================================== +[·] [1/5] 建目标库... + · 检查/准备 sqlite 文件... + · 删除旧 sqlite 文件... +[✓] [1/5] 建目标库 完成(耗时 8ms) +[·] [2/5] 建表结构(23 张表 create_all)... +[✓] [2/5] 建表结构 完成(耗时 96ms),23 张表 +[·] [3/5] 逐表迁移数据... + [✓ tornado_city] 共 4150 行(耗时 850ms) + ... +[✓] [3/5] 逐表迁移数据 完成(耗时 1.2s),共 4843 行 +[·] [4/5] 重置自增序列... + · MySQL/SQLite 自动跟随,无需处理 +[✓] [4/5] 重置自增序列 完成(耗时 0ms) +[·] [5/5] 逐表行数校验... +[✓] [5/5] 逐表行数校验 完成(耗时 100ms),23 张表全部 PASS +============================================================== +[迁移完成] 成功:4843 行 / 23 张表 / 总耗时 1.4s +============================================================== +``` + +### MySQL → Oracle + +```bash +python scripts/migrate_db.py \ + --src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \ + --src-db=djangoadmin.tornado.elevue2 --src-user=root --src-pass=root \ + --dst-driver=oracle --dst-host=localhost --dst-port=1521 \ + --dst-db=ORCLPDB1 --dst-user=tornado --dst-pass=你的密码 \ + --drop-target-first +``` + +:::warning Oracle 特殊说明 +- Oracle 无"数据库名",`--dst-db` 填 **SERVICE_NAME**(如 `ORCLPDB1`、`XE`) +- schema 即 `--dst-user` 连接的用户(无需也无法"建库",连上即视为就绪) +- 该用户需有 `CONNECT`、`RESOURCE` 权限 +- `create_all` 对 Integer 主键只生成 `INTEGER NOT NULL`(无 identity),迁移后脚本自动补 sequence(`START WITH MAX(id)+1`)+ `BEFORE INSERT` 触发器 +- `--drop-target-first` 时尽力 `DROP TABLE ... PURGE` 清理旧表 +::: + +输出示例: + +``` +============================================================== +[迁移开始] 源: mysql → 目标: oracle:ORCLPDB1 +[迁移开始] 待迁移 23 张表 +============================================================== +[·] [1/5] 建目标库... + · 正在连接 Oracle 服务 ORCLPDB1(schema=tornado)... + · drop-first:清理 23 张旧项目表(DROP TABLE ... PURGE)... +[✓] [1/5] 建目标库 完成(耗时 4.6s) +[·] [2/5] 建表结构(23 张表 create_all)... +[✓] [2/5] 建表结构 完成(耗时 2.4s),23 张表 +[·] [3/5] 逐表迁移数据... + [✓ tornado_city] 共 4150 行(耗时 2m12s) + ... +[✓] [3/5] 逐表迁移数据 完成(耗时 2m41s),共 4843 行 +[·] [4/5] 重置自增序列... ← 补 sequence + BEFORE INSERT 触发器 +[✓] [4/5] 重置自增序列 完成(耗时 6.2s) +[·] [5/5] 逐表行数校验... +[✓] [5/5] 逐表行数校验 完成(耗时 2.6s),23 张表全部 PASS +============================================================== +[迁移完成] 成功:4843 行 / 23 张表 / 总耗时 2m54s +============================================================== +``` + +### 幂等重跑 + +迁移失败或需重跑时,加 `--drop-target-first` 可安全重跑: + +```bash +python scripts/migrate_db.py ... --drop-target-first +``` + +:::warning 警告 +`--drop-target-first` 会删除目标库所有表(Oracle 清旧表、SQLite 删旧文件),仅在确认目标库无生产数据时使用。 +::: + +### 断点续传 + +迁移过程被中断时,已迁移的表按每表一个事务提交,未迁完的表可配合 `--only-tables` 和 `--skip-existing` 断点续传: + +```bash +# 只迁移未完成的表 +python scripts/migrate_db.py ... --only-tables user,role,article --skip-existing +``` + +### 迁移拓扑顺序 + +数据泵按拓扑序迁移,确保引用完整性: + +``` +基础表(先迁移):dept, menu, role, level, position, city, category, dict, config, ... +依赖表(后迁移):role_menu, dict_item, config_item, user, user_role, article, notice, ... +``` + +:::tip 无外键设计 +数据库不使用物理外键,依赖关系通过业务代码维护。迁移时显式带 id 插入,保住引用完整性。 +::: + +### 各驱动自增序列处理 + +| 驱动 | 处理方式 | 说明 | +|:----|:----|:----| +| PostgreSQL | `setval(pg_get_serial_sequence(...), MAX(id))` | SERIAL 序列重置到最大值 | +| SQL Server | `IDENTITY_INSERT ON` + `DBCC CHECKIDENT RESEED` | 显式插 id + 重置种子 | +| Oracle | 补 sequence + BEFORE INSERT 触发器 | `create_all` 无原生自增,脚本自动补建 | +| MySQL / SQLite | 自动跟随 | 显式插 id 后 auto_increment / rowid 自动跟随 | + +如需手动修复自增: + +```sql +-- PostgreSQL +SELECT setval(pg_get_serial_sequence('tornado_user', 'id'), (SELECT max(id) FROM tornado_user)); + +-- SQL Server +DBCC CHECKIDENT ('tornado_user', RESEED, ) + +-- Oracle +DROP SEQUENCE tornado_user_id_seq; +CREATE SEQUENCE tornado_user_id_seq START WITH ; +``` + +## 常见问题与排错 + +| 问题 | 原因 | 解决方案 | +|:----|:----|:----| +| 目标库数据叠加/主键冲突 | 未加 `--drop-target-first`,已有数据与迁移数据冲突 | 加 `--drop-target-first` 重跑 | +| 迁移后报 "no such table" | `--dst-db` 指向错误 | Oracle 填 SERVICE_NAME,SQLite 填文件路径 | +| 中文乱码 | 目标库编码非 UTF8 | 删除重建(`--drop-target-first`),PG 建库强制 UTF8 | +| Oracle 新增记录 id 异常 | sequence 未正确创建 | 脚本自动补建;若手动建过同名 sequence 会先删后建 | +| 自增 id 未延续原值 | 序列未重置 | 参考上表手动修复 | +| 迁移中断 | 网络或超时 | 配合 `--only-tables` + `--skip-existing` 断点续传 | + +## 常见操作速查 + +| 场景 | 命令 | +|------|------| +| 全新安装 | `make db-init` | +| 模型变更后生成迁移 | `make migrate-revision` | +| 应用迁移 | `make migrate-upgrade` | +| 回滚一次迁移 | `alembic downgrade -1` | +| 标记已有库为最新 | `make migrate-stamp` | +| 跨库迁移干跑 | `make db-migrate-dry-run ARGS="..."` | +| 跨库迁移执行 | `make db-migrate ARGS="..."` | + +## 总结 + +数据库迁移分为结构迁移(Alembic)和数据迁移(`migrate_db.py`)两部分。全新安装用 `make db-init`,模型变更用 Alembic 自动生成迁移脚本,跨库切换用 `migrate_db.py` 数据泵。建议每次迁移前先干跑确认,生产环境务必做好备份。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume6/deploy-checklist.md b/DjangoAdmin_Tornado_EleVue/zh/volume6/deploy-checklist.md new file mode 100644 index 0000000..57546fe --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume6/deploy-checklist.md @@ -0,0 +1,206 @@ +:::tip 本章概要 +从服务器准备到应用上线的完整部署操作清单,适用于生产环境首次部署和后续版本更新。 +::: + +# 部署 Checklist + +本章提供从服务器准备到应用上线的完整部署操作清单,适用于生产环境首次部署和后续版本更新。 + +## 首次部署 Checklist + +### 第 1 阶段:服务器准备 + +- [ ] 服务器操作系统:CentOS 7+ / Ubuntu 20.04+ / Debian 11+ +- [ ] 防火墙开放端口:80(HTTP)、443(HTTPS)、22(SSH) +- [ ] 创建部署用户(非 root) +- [ ] 配置 SSH 密钥登录 +- [ ] 配置时区:`timedatectl set-timezone Asia/Shanghai` + +### 第 2 阶段:环境安装 + +**Python 环境**: + +- [ ] 安装 Python 3.12+ +- [ ] 安装 pip:`apt install python3-pip` 或 `yum install python3-pip` +- [ ] 创建虚拟环境:`python3 -m venv /opt/venv` +- [ ] 激活虚拟环境:`source /opt/venv/bin/activate` +- [ ] 安装依赖:`pip install -r requirements.txt` + +**数据库**: + +- [ ] 安装 MySQL 8.0+ +- [ ] 创建数据库:`CREATE DATABASE djangoadmin.tornado.elevue2 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;` +- [ ] 创建数据库用户并授权 +- [ ] 修改 `.env` 中的数据库配置 +- [ ] 初始化数据库:`python scripts/init_db.py` + +**Redis**: + +- [ ] 安装 Redis 6.0+ +- [ ] 配置密码 +- [ ] 修改 `.env` 中的 Redis 配置 +- [ ] 测试连接:`redis-cli ping` + +**Node.js(前端构建)**: + +- [ ] 安装 Node.js 18+ +- [ ] 安装 pnpm:`npm install -g pnpm` +- [ ] 安装前端依赖:`cd ui && pnpm install` +- [ ] 构建前端:`pnpm build` + +### 第 3 阶段:应用部署 + +**后端**: + +- [ ] 上传项目代码到服务器 +- [ ] 配置 `.env` 文件(数据库、Redis、JWT_SECRET 等) +- [ ] 生成 JWT 密钥:`python -c "import secrets; print(secrets.token_urlsafe(48))"` +- [ ] 测试启动:`python app.py` +- [ ] 配置 Supervisor 进程管理 +- [ ] 启动后端服务:`supervisorctl start djangoadmin-tornado` + +**前端**: + +- [ ] 构建产物上传到服务器(`ui/dist/` 目录) +- [ ] 配置 Nginx 指向前端产物目录 +- [ ] 配置 Nginx 反向代理后端 API + +### 第 4 阶段:Nginx 配置 + +```nginx +server { + listen 80; + server_name your-domain.com; + + # 前端静态文件 + root /opt/project/ui/dist; + index index.html; + + # 前端路由 + location / { + try_files $uri $uri/ /index.html; + } + + # 后端 API 反向代理 + location /api/ { + proxy_pass http://127.0.0.1:8041; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # 上传文件访问 + location /uploads/ { + alias /opt/project/uploads/; + expires 30d; + } +} +``` + +- [ ] 配置 Nginx +- [ ] 测试 Nginx 配置:`nginx -t` +- [ ] 重载 Nginx:`nginx -s reload` + +### 第 5 阶段:SSL 证书(推荐) + +- [ ] 申请 SSL 证书(Let's Encrypt / 购买) +- [ ] 配置 HTTPS +- [ ] 配置 HTTP 自动跳转 HTTPS +- [ ] 测试 HTTPS 访问 + +### 第 6 阶段:验证 + +- [ ] 访问前端页面,确认正常加载 +- [ ] 登录测试(admin / 123456) +- [ ] 修改默认密码 +- [ ] 测试核心功能(用户管理、角色管理等) +- [ ] 检查后端日志:`tail -f logs/app.log` +- [ ] 检查 Supervisor 状态:`supervisorctl status` + +## 版本更新 Checklist + +### 后端更新 + +- [ ] 拉取最新代码:`git pull origin main` +- [ ] 安装新依赖(如有):`pip install -r requirements.txt` +- [ ] 执行数据库迁移(如有):`alembic upgrade head` +- [ ] 重启后端服务:`supervisorctl restart djangoadmin-tornado` +- [ ] 检查日志:`tail -f logs/app.log` + +### 前端更新 + +- [ ] 拉取最新代码:`git pull origin main` +- [ ] 安装新依赖(如有):`pnpm install` +- [ ] 构建前端:`pnpm build` +- [ ] 替换服务器上的 `ui/dist/` 目录 +- [ ] 清除浏览器缓存测试 + +### 数据库迁移 + +- [ ] 备份数据库:`mysqldump -uroot -p djangoadmin.tornado.elevue2 > backup.sql` +- [ ] 执行迁移:`alembic upgrade head` +- [ ] 验证迁移结果 + +## Docker 部署 Checklist + +- [ ] 安装 Docker 和 Docker Compose +- [ ] 配置 `.env` 文件 +- [ ] 构建镜像:`docker-compose build` +- [ ] 启动服务:`docker-compose up -d` +- [ ] 检查容器状态:`docker-compose ps` +- [ ] 查看日志:`docker-compose logs -f` +- [ ] 初始化数据库(首次):`docker-compose exec app python scripts/init_db.py` + +## 监控 Checklist + +- [ ] 配置健康检查端点 +- [ ] 配置日志轮转(logrotate) +- [ ] 配置数据库自动备份(cron) +- [ ] 配置 Redis 持久化(AOF/RDB) +- [ ] 配置磁盘空间监控 +- [ ] 配置内存使用监控 + +## 安全 Checklist + +- [ ] 修改默认密码(admin / 123456) +- [ ] 设置强密码的 JWT_SECRET +- [ ] 设置 `TORNADO_DEBUG=False` +- [ ] 设置 `TORNADO_DEMO=False`(生产环境) +- [ ] 配置 CORS 白名单(非 `*`) +- [ ] 配置 HTTPS +- [ ] 关闭不必要的端口 +- [ ] 定期更新系统和依赖包 + +## 回滚方案 + +### 后端回滚 + +```bash +# 回滚代码 +git checkout <上一个稳定版本tag> + +# 回滚数据库迁移 +alembic downgrade -1 + +# 重启服务 +supervisorctl restart djangoadmin-tornado +``` + +### 前端回滚 + +```bash +# 替换为上一版本的构建产物 +cp -r /opt/backup/dist /opt/project/ui/dist +``` + +### 数据库回滚 + +```bash +# 从备份恢复 +mysql -uroot -p djangoadmin.tornado.elevue2 < backup.sql +``` + +## 总结 + +部署 Checklist 覆盖了从服务器准备到应用上线的完整流程。首次部署建议按顺序逐项检查,版本更新时使用更新 Checklist 快速操作。生产环境务必做好备份和监控。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume6/docker.md b/DjangoAdmin_Tornado_EleVue/zh/volume6/docker.md new file mode 100644 index 0000000..5921bb5 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume6/docker.md @@ -0,0 +1,206 @@ +# Docker容器化 + +## 概述 + +提供完整的 Docker 容器化方案,包括 Dockerfile 镜像构建和 docker-compose.yml 编排配置,支持一键部署后端服务。 + +:::tip 容器化优势 +- 环境一致性:消除"在我机器上能跑"的问题 +- 快速部署:一条命令启动完整服务 +- 资源隔离:应用与宿主机环境解耦 +::: + +## Dockerfile 详解 + +项目根目录 `Dockerfile` 完整内容: + +```dockerfile +# 基础镜像:Python 3.11-slim(python:3.9 已于 2025-10 停止安全更新) +FROM python:3.11-slim + +LABEL maintainer="管理员" + +# 环境变量:禁用 Python 缓存和字节码写入 +ENV PYTHONUNBUFFERED 1 +ENV PYTHONDONTWRITEBYTECODE 1 +ENV TZ Asia/Shanghai + +# 国内镜像源加速(使用 https,避免 http 明文传输被篡改) +RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple +RUN pip config set install.trusted-host mirrors.aliyun.com + +# 工作目录 +WORKDIR /data/apps + +# 先拷贝依赖清单并安装,利用 Docker 层缓存(源码变更不会触发重复安装依赖) +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# 拷贝全部源码(.dockerignore 已排除上传目录/缓存/测试等,避免镜像膨胀) +COPY . /data/apps + +# 暴露应用端口 +EXPOSE 8041 + +# 启动命令:Tornado 为 ASGI 应用,使用 tornado.httpserver +# 绑定 0.0.0.0 使 docker run -p 8041:8041 可直接访问; +# compose 场景下由 docker-compose.yml 的 command 覆盖 +CMD ["tornado.httpserver", "main:app", "--app-dir", "src", "--host", "0.0.0.0", "--port", "8041"] +``` + +:::tip 构建优化 +Dockerfile 将 `requirements.txt` 和源码分两步 COPY,利用 Docker 层缓存机制——源码变更不会触发依赖重新安装,显著加快构建速度。镜像源使用 HTTPS 协议,防止明文传输被篡改。 +::: + +## docker-compose.yml 结构 + +```yaml +version: '3.7' + +services: + tornado_elevue: # 服务名称 + image: tornado_elevue # 镜像名称 + container_name: tornado_elevue # 容器名称 + build: # 构建配置 + context: ./ # 构建上下文路径 + dockerfile: Dockerfile # Dockerfile 文件名 + restart: always # 重启策略:总是重启 + network_mode: host # 网络模式:使用宿主机网络 + ports: # 端口映射 + - 8041:8041 # 宿主机端口:容器端口 + volumes: # 数据卷挂载 + - $PWD/uploads:/data/apps/uploads # 上传文件持久化存储 + env_file: # 环境变量文件 + - .env # 加载 .env 文件中的环境变量 + command: python app.py # 容器启动命令 + logging: # 日志配置 + driver: "json-file" # 日志驱动:json-file + options: + max-size: "500m" # 单个日志文件最大大小 + max-file: "10" # 保留日志文件数量 +``` + +### 配置说明 + +| 配置项 | 说明 | +|--------|------| +| `restart: always` | 容器异常退出后自动重启 | +| `network_mode: host` | 使用宿主机网络,可直接访问 localhost 的 MySQL/Redis | +| `volumes` | 挂载上传目录,确保文件持久化 | +| `env_file` | 从 `.env` 注入环境变量 | +| `logging` | 日志轮转:单文件最大 500MB,保留 10 个文件 | + +:::warning 网络模式说明 +`host` 模式下 `ports` 映射被忽略(容器直接使用宿主机端口)。若改为 `bridge` 网络,需将 `DB_HOST`/`REDIS_HOST` 指向 `host.docker.internal` 并启用端口映射。 +::: + +## 常用操作命令 + +### 构建与启动 + +```bash +# 构建镜像并启动 +docker-compose up --build + +# 后台启动 +docker-compose up --build -d + +# 仅构建镜像 +docker-compose build +``` + +### 日常管理 + +```bash +# 查看运行状态 +docker-compose ps + +# 查看日志 +docker-compose logs -f tornado_elevue + +# 停止服务 +docker-compose down + +# 重启服务 +docker-compose restart tornado_elevue + +# 进入容器 +docker exec -it tornado_elevue bash +``` + +### 查看容器资源 + +```bash +# 查看容器资源占用 +docker stats tornado_elevue + +# 查看容器日志大小 +docker inspect --format='{{.LogPath}}' tornado_elevue +``` + +## 环境变量注入 + +Docker 通过 `env_file: .env` 将宿主机的 `.env` 文件注入容器。容器内的应用通过 `os.getenv()` 读取,与非 Docker 部署完全一致。 + +:::tip .env 文件位置 +`.env` 文件应放在 `docker-compose.yml` 同级目录下。部署时目录结构: +``` +/data/apps/djangoadmin +├── .env # 环境变量 +├── Dockerfile +├── docker-compose.yml +├── requirements.txt +├── src/ +└── uploads/ # 上传文件目录(挂载卷) +``` +::: + +## 上传文件持久化 + +通过 `volumes` 将容器内的上传目录挂载到宿主机,确保容器重建后文件不丢失: + +```yaml +volumes: + - $PWD/uploads:/data/apps/uploads +``` + +## 日志管理 + +容器日志使用 `json-file` 驱动,配置轮转防止磁盘占满: + +```yaml +logging: + driver: "json-file" + options: + max-size: "500m" # 单文件最大 500MB + max-file: "10" # 保留 10 个文件 +``` + +:::tip 生产环境日志 +生产环境建议将日志输出到 stdout/stderr,由 Docker 日志驱动统一收集。如需集中式日志,可配置 `fluentd` 或 `syslog` 驱动对接 ELK/Loki。 +::: + +## 多容器编排(扩展) + +如需同时部署前端和 Nginx,可扩展 `docker-compose.yml`: + +```yaml +services: + tornado_elevue: + # ... 后端配置 ... + + nginx: + image: nginx:alpine # Nginx 镜像(轻量版) + ports: # 端口映射 + - 80:80 # HTTP + - 443:443 # HTTPS + volumes: # 挂载配置 + - ./nginx.conf:/etc/nginx/conf.d/default.conf # Nginx 配置文件 + - ./dist:/usr/share/nginx/html # 前端构建产物 + depends_on: # 依赖服务 + - tornado_elevue # 等待后端服务启动 +``` + +## 总结 + +Docker 容器化通过 Dockerfile 构建镜像、docker-compose.yml 编排服务,实现一键部署。关键点:利用层缓存优化构建速度、host 网络模式简化数据库连接、volumes 持久化上传文件、日志轮转防磁盘占满。生产环境建议配合 Nginx 反向代理使用。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume6/env-plan.md b/DjangoAdmin_Tornado_EleVue/zh/volume6/env-plan.md new file mode 100644 index 0000000..758d000 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume6/env-plan.md @@ -0,0 +1,132 @@ +# 环境规划 + +## 概述 + + 项目支持多环境部署,通过 `.env` 文件管理各环境的配置差异。合理的环境规划能确保开发、测试、生产环境的隔离性,降低配置错误导致的线上风险。 + +:::tip 核心原则 +各环境之间保持代码一致,仅通过环境变量(`.env`)区分配置。禁止在代码中硬编码环境相关的值。 +::: + +## 环境划分 + +| 环境 | 用途 | 域名示例 | DEBUG | 数据库 | Redis | 日志级别 | +|------|------|---------|-------|--------|-------|---------| +| Dev(开发) | 本地开发调试 | localhost:8041 | True | 开发库 | 开发实例 | DEBUG | +| Test(测试) | 功能/集成测试 | test.example.com | False | 测试库 | 测试实例 | INFO | +| Prod(生产) | 正式对外服务 | www.example.com | False | 生产库 | 生产集群 | WARNING | + +:::tip 环境隔离要求 +1. 三个环境的数据库必须完全隔离,禁止共用同一库。 +2. Redis 实例建议分离,避免测试数据污染生产缓存。 +3. 生产环境的 `.env` 文件权限应设为 `600`,仅运行用户可读。 +::: + +## .env 配置示例 + +### Dev 环境 + +```bash +# 应用配置 +TORNADO_ENV=dev +TORNADO_DEBUG=True +TORNADO_DEMO=False +TORNADO_PORT=8041 + +# 数据库配置 +DB_DRIVER=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_USER=root +DB_PASS=root +DB_NAME=djangoadmin_dev +DB_PREFIX=tornado_ + +# Redis 配置 +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 +REDIS_DB=0 + +# JWT 配置 +JWT_SECRET=dev-salt-change-in-production +JWT_EXPIRE_MINUTES=1440 +``` + +### Prod 环境 + +```bash +# 应用配置 +TORNADO_ENV=prod +TORNADO_DEBUG=False +TORNADO_DEMO=False +TORNADO_PORT=8041 + +# 数据库配置 +DB_DRIVER=mysql +DB_HOST=10.0.1.100 +DB_PORT=3306 +DB_USER=djangoadmin +DB_PASS=<强密码> +DB_NAME=djangoadmin_prod +DB_PREFIX=tornado_ + +# Redis 配置 +REDIS_HOST=10.0.1.101 +REDIS_PORT=6379 +REDIS_PASSWORD=<强密码> +REDIS_DB=0 + +# JWT 配置 +JWT_SECRET=<随机生成的64位字符串> +JWT_EXPIRE_MINUTES=720 +``` + +:::tip 安全提醒 +生产环境的 `JWT_SECRET` 必须使用随机强密码(推荐 64 位以上),禁止与开发环境共用同一值。可通过以下命令生成: +```bash +python -c "import secrets; print(secrets.token_urlsafe(48))" +``` +::: + +## 环境切换机制 + +项目通过 `python-dotenv` 加载 `.env` 文件,启动时自动读取当前目录下的 `.env`: + +```bash +# Dev:本地启动 +python app.py + +# Prod:指定 .env 路径(或在部署目录放置 .env) +cd /data/apps/djangoadmin && python app.py +``` + +:::tip Docker 环境 +Docker 部署时,`docker-compose.yml` 通过 `env_file: .env` 注入环境变量,容器内无需额外配置。详见 [Docker容器化](/zh/volume6/docker) 章节。 +::: + +## 数据库驱动切换 + +通过 `DB_DRIVER` 环境变量切换数据库类型(配置在 `src/config/database.py`): + +| DB_DRIVER | 数据库 | 依赖包 | 连接串格式 | +|-----------|--------|--------|-----------| +| mysql(默认) | MySQL 8.x | PyMySQL | `mysql+pymysql://user:pass@host:3306/db` | +| postgresql | PostgreSQL 14+ | psycopg[binary] | `postgresql+psycopg://user:pass@host:5432/db` | +| mssql | SQL Server 2019+ | pymssql | `mssql+pymssql://user:pass@host:1433/db` | +| sqlite | SQLite 3 | 内置 | `sqlite:///path/to/db.sqlite3` | +| oracle | Oracle 12c+ | oracledb | `oracle+oracledb://user:pass@host:1521/db` | + +:::tip 切换数据库后 +切换数据库驱动后,需要重新初始化数据库结构。项目 Makefile 提供常用命令: +```bash +make db-init # 初始化数据库(建库 + SQL 导入 + 补表 + alembic stamp) +make migrate-revision # 生成增量迁移脚本 +make migrate-upgrade # 应用迁移 +make migrate-stamp # 标记当前库已到基线 +make db-migrate ARGS="..." # 跨库数据迁移 +``` +::: + +## 总结 + +环境规划的核心是隔离与一致性:代码统一,配置分离。通过 `.env` 文件管理各环境差异,`DB_DRIVER` 支持多数据库切换。生产环境务必关注安全配置(DEBUG=False、强密码、JWT_SECRET 独立),确保线上服务稳定可靠。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume6/monitoring.md b/DjangoAdmin_Tornado_EleVue/zh/volume6/monitoring.md new file mode 100644 index 0000000..36b0a67 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume6/monitoring.md @@ -0,0 +1,164 @@ +# 健康检查与监控 + +## 概述 + +生产环境的健康检查与监控是保障服务可用性的关键环节。本文介绍 Tornado 应用的健康检查端点、监控指标采集、日志聚合方案。 + +:::tip 监控体系层次 +``` +1. 应用层:健康检查端点、业务指标 +2. 系统层:CPU/内存/磁盘/网络 +3. 中间件:MySQL/Redis 连接状态 +4. 日志:集中式日志收集与分析 +``` +::: + +## 健康检查端点 + +Tornado 内置健康检查路由,可用于负载均衡器或容器编排的探针配置: + +```bash +# 基础健康检查 +GET /api/v1/health + +# 响应示例 +{ + "code": 0, + "data": { + "status": "healthy", + "version": "1.0.0", + "uptime": 3600 + }, + "msg": "操作成功", + "ok": true +} +``` + +:::tip Docker 健康检查 +在 Dockerfile 中配置 HEALTHCHECK: +```dockerfile +HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ + CMD curl -f http://localhost:8041/api/v1/health || exit 1 +``` +::: + +## 监控指标 + +### 应用指标 + +| 指标 | 说明 | 采集方式 | +|------|------|---------| +| 请求总量 | API 调用次数 | Nginx access log / 应用中间件 | +| 响应时间 | P50/P95/P99 延迟 | 应用中间件 / APM | +| 错误率 | 5xx 响应占比 | Nginx error log / 应用日志 | +| 活跃连接数 | 当前并发请求 | Tornado stats | + +### 系统指标 + +| 指标 | 说明 | 告警阈值 | +|------|------|---------| +| CPU 使用率 | 进程 CPU 占用 | > 80% | +| 内存使用率 | 进程内存占用 | > 85% | +| 磁盘使用率 | 磁盘空间占用 | > 90% | +| 网络流量 | 入站/出站带宽 | 根据带宽设定 | + +### 中间件指标 + +| 指标 | 说明 | 告警阈值 | +|------|------|---------| +| MySQL 连接数 | 活跃/空闲连接 | > max_connections * 0.8 | +| MySQL 慢查询 | 执行时间 > 1s 的查询 | > 10/min | +| Redis 内存使用 | Redis 内存占用 | > maxmemory * 0.8 | +| Redis 连接数 | 活跃客户端连接 | > maxclients * 0.8 | + +## 日志聚合 + +### 应用日志格式 + +项目使用 Python `logging` 模块输出结构化日志: + +``` +2026-09-05 10:30:15 [INFO] core.app: Application startup complete +2026-09-05 10:30:16 [WARNING] middleware.rate_limit: Rate limit exceeded for 192.168.1.100 +2026-09-05 10:30:17 [ERROR] modules.auth.service: Login failed for user admin +``` + +### ELK 方案 + +Elasticsearch + Logstash + Kibana 集中日志收集: + +```yaml +# Filebeat 配置示例 +filebeat.inputs: + - type: log + enabled: true + paths: + - /data/apps/logs/*.log + fields: + app: djangoadmin + multiline.pattern: '^\d{4}-\d{2}-\d{2}' + multiline.negate: true + multiline.match: after + +output.elasticsearch: + hosts: ["http://elasticsearch:9200"] + index: "djangoadmin-%{+yyyy.MM.dd}" +``` + +### Loki 方案 + +轻量级日志聚合(配合 Grafana): + +```yaml +# docker-compose.yml 添加 Loki 和 Grafana +services: + loki: + image: grafana/loki:latest + ports: + - "3100:8001" + + promtail: + image: grafana/promtail:latest + volumes: + - /data/apps/logs:/var/log/app + - ./promtail-config.yml:/etc/promtail/config.yml + + grafana: + image: grafana/grafana:latest + ports: + - "3000:3000" +``` + +:::tip Loki 优势 +相比 ELK,Loki 更轻量——不索引日志内容,仅索引标签,存储成本低,适合中小规模部署。 +::: + +## 告警配置 + +### 常用告警规则 + +```yaml +# Prometheus AlertManager 规则示例 +groups: + - name: djangoadmin + rules: + - alert: HighErrorRate + expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.1 + for: 5m + labels: + severity: critical + annotations: + summary: "错误率超过10%" + + - alert: HighResponseTime + expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 2 + for: 5m + labels: + severity: warning + annotations: + summary: "P95响应时间超过2秒" +``` + +## 总结 + +健康检查与监控是生产环境的基础设施:通过健康检查端点实现负载均衡探针,通过系统/应用/中间件指标全面掌握服务状态,通过 ELK 或 Loki 实现日志集中分析。建议中小项目优先采用 Loki+Grafana 轻量方案,大型项目使用 ELK 全家桶。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume6/nginx.md b/DjangoAdmin_Tornado_EleVue/zh/volume6/nginx.md new file mode 100644 index 0000000..4f27640 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume6/nginx.md @@ -0,0 +1,177 @@ +# Nginx 反向代理 + +## 概述 + +Nginx 作为反向代理服务器,承担静态资源服务、API 请求转发、HTTPS 终端等职责。生产环境推荐使用 Nginx 作为前端入口,将 API 请求代理到后端 Tornado 服务。 + +:::tip 架构示意 +``` +客户端 → Nginx(80/443) + ├─ / → 前端静态资源(dist/) + ├─ /api/v1/... → Tornado 后端(8041) + └─ /api/file/ → 上传文件目录 +``` +::: + +## 基础配置 + +项目提供了 Nginx 配置模板 `deploy/nginx.conf`,按需修改即可使用: + +```nginx +upstream tornado_backend { + server 127.0.0.1:8041; + keepalive 16; +} + +server { + listen 80; + server_name your-domain.com; + + charset utf-8; + client_max_body_size 50m; + + # 安全头 + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + + # Gzip 压缩 + gzip on; + gzip_min_length 1k; + gzip_comp_level 6; + gzip_types text/plain text/css application/json application/javascript text/xml image/svg+xml; + gzip_vary on; + + # 前端静态资源 + root /data/apps/djangoadmin/frontend/dist; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /assets/ { + expires 30d; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # API 反向代理 + location /api/ { + proxy_pass http://tornado_backend/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Connection ""; + proxy_connect_timeout 60s; + proxy_read_timeout 120s; + proxy_send_timeout 60s; + } + + # 上传文件访问 + location /api/file/ { + alias /data/apps/djangoadmin/uploads/; + expires 7d; + access_log off; + limit_except GET { deny all; } + } + + # 禁止访问隐藏文件 + location ~ /\. { deny all; } +} +``` + +:::tip 配置说明 +- `upstream` + `keepalive`:长连接池减少 TCP 握手开销 +- `location /`:`try_files` 支持 SPA 路由回退 +- `location /api/`:只代理 API 请求,前端静态资源由 Nginx 直接服务 +- `location /api/file/`:通过 `alias` 访问上传文件目录,只允许 GET +- `client_max_body_size 50m`:与后端 `UPLOAD_MAX_SIZE_MB` 保持一致 +- `proxy_read_timeout 120s`:适应大数据量导出等长耗时操作 +::: + +## 部署步骤 + +```bash +# 1. 前端构建 +cd ui && pnpm build + +# 2. 拷贝构建产物 +mkdir -p /data/apps/djangoadmin/frontend +cp -r ui/dist /data/apps/djangoadmin/frontend/ + +# 3. 拷贝 Nginx 配置 +cp deploy/nginx.conf /etc/nginx/conf.d/djangoadmin.conf + +# 4. 修改配置中的实际值(server_name、root 等) + +# 5. 检查并重载 +nginx -t && nginx -s reload +``` + +## HTTPS 配置 + +```nginx +server { + listen 443 ssl http2; + server_name your-domain.com; + + ssl_certificate /etc/nginx/ssl/cert.pem; + ssl_certificate_key /etc/nginx/ssl/key.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + + # HSTS(强制 HTTPS) + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + # ... 其他 location 配置同上 ... +} + +# HTTP 自动跳转 HTTPS +server { + listen 80; + server_name your-domain.com; + return 301 https://$server_name$request_uri; +} +``` + +:::warning SSL 证书安全 +- 证书文件权限设为 `600`,仅 Nginx 用户可读 +- 禁用 TLSv1.0/1.1,仅保留 TLSv1.2+ +- 推荐 Let's Encrypt 免费证书 + certbot 自动续期 +::: + +## 常用命令 + +```bash +nginx -t # 检查配置语法 +nginx -s reload # 重新加载(不停服) +nginx -s stop # 停止 +nginx -v # 查看版本 +``` + +## 常见问题 + +### 502 Bad Gateway + +后端 Tornado 服务未启动或端口不对。检查:`curl http://127.0.0.1:8041/api/v1/captcha` + +### 413 Request Entity Too Large + +上传文件超过 `client_max_body_size` 限制,调大该值与后端 `UPLOAD_MAX_SIZE_MB` 一致。 + +### 刷新页面 404 + +缺少 `try_files $uri $uri/ /index.html`,SPA 路由需要回退到 `index.html`。 + +### API 请求走到了前端 + +`location /api/` 必须在 `location /` 之前定义,Nginx 按优先级匹配。 + +## 总结 + +Nginx 反向代理是生产部署的标准配置:`/` 服务前端静态资源、`/api/` 反向代理到 Tornado 后端、`/api/file/` 访问上传文件。关键配置包括 `try_files` 支持 SPA 路由、`client_max_body_size` 匹配上传限制、Gzip 压缩和静态资源缓存。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume6/performance.md b/DjangoAdmin_Tornado_EleVue/zh/volume6/performance.md new file mode 100644 index 0000000..f9f6093 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume6/performance.md @@ -0,0 +1,208 @@ +# 性能调优 + +## 概述 + +Tornado 应用的性能调优涉及 Tornado HTTPServer 配置、数据库连接池、Redis 缓存策略、慢查询优化等多个层面。合理的调优能显著提升并发处理能力和响应速度。 + +:::tip 性能调优原则 +1. 先测量,后优化:用数据说话,避免盲目调参 +2. 瓶颈优先:找到最大瓶颈,集中优化 +3. 渐进式:每次只调整一个参数,观察效果 +::: + +## Tornado HTTPServer 配置 + +### Worker 数量 + +```bash +# 单 Worker(开发环境) +python app.py + +# 多 Worker(生产环境) +tornado.httpserver main:app --app-dir src --host 0.0.0.0 --port 8041 --workers 4 +``` + +:::tip Worker 数量公式 +推荐 Worker 数量 = `CPU 核心数 * 2 + 1` + +例如 4 核 CPU:`workers = 4 * 2 + 1 = 9` + +查看 CPU 核心数: +```bash +# Linux +nproc + +# Python +import multiprocessing; print(multiprocessing.cpu_count()) +``` +::: + +### 启动方式选择 + +| 方式 | 适用场景 | 特点 | +|------|---------|------| +| `python app.py` | 开发调试 | 单进程,自动重载 | +| `tornado.httpserver --workers N` | 生产部署 | 多进程,无自动重载 | +| `python app.py` | 生产部署 | 更成熟的进程管理 | + +### Gunicorn 配置示例 + +```bash +pip install gunicorn + +gunicorn main:app \ + --chdir src \ + --workers 4 \ + ----port=8041 \ + --bind 0.0.0.0:8041 \ + --timeout 120 \ + --keep-alive 5 \ + --max-requests 1000 \ + --max-requests-jitter 50 +``` + +:::tip max-requests 作用 +`--max-requests 1000` 表示每个 Worker 处理 1000 个请求后自动重启,防止内存泄漏累积。`--max-requests-jitter 50` 添加随机抖动,避免所有 Worker 同时重启。 +::: + +## 数据库连接池 + +SQLAlchemy 连接池配置(在 `core/config/database.py` 中): + +```python +# 连接池配置 +SQLALCHEMY_POOL_SIZE = 20 # 连接池大小 +SQLALCHEMY_MAX_OVERFLOW = 10 # 超出池大小的最大连接数 +SQLALCHEMY_POOL_TIMEOUT = 30 # 获取连接超时(秒) +SQLALCHEMY_POOL_RECYCLE = 3600 # 连接回收时间(秒) +``` + +:::warning 连接池大小 +连接池总连接数 = `pool_size + max_overflow`。需确保不超过数据库的 `max_connections` 限制,否则会报连接数超限错误。 + +MySQL 查看最大连接数: +```sql +SHOW VARIABLES LIKE 'max_connections'; +``` +::: + +### 连接池监控 + +```python +# 获取连接池状态 +from core.database import engine +pool = engine.pool +print(f"池大小: {pool.size()}") +print(f"已签出: {pool.checkedout()}") +print(f"空闲数: {pool.checkedin()}") +print(f"溢出数: {pool.overflow()}") +``` + +## Redis 缓存优化 + +### 缓存策略 + +| 策略 | 适用场景 | 实现方式 | +|------|---------|---------| +| 读缓存 | 字典、配置等读多写少 | 查询时先读 Redis,未命中再查 DB | +| 写失效 | 数据更新时 | 更新 DB 后删除 Redis 缓存 | +| 预热 | 服务启动时 | 启动后主动加载热点数据到 Redis | + +### TTL 设置建议 + +```python +# 字典数据:较长 TTL(变更频率低) +DICT_CACHE_TTL = 3600 # 1 小时 + +# 用户权限:中等 TTL(登录时刷新) +PERM_CACHE_TTL = 1800 # 30 分钟 + +# 验证码:短 TTL(一次性使用) +CAPTCHA_CACHE_TTL = 300 # 5 分钟 +``` + +### 缓存预热 + +```python +# 服务启动时预热缓存 +async def warmup_cache(): + """预热字典、配置等热点数据""" + from utils.dict_util import refresh_dict_cache + await refresh_dict_cache() +``` + +## 慢查询优化 + +### 开启慢查询日志 + +MySQL: +```sql +SET GLOBAL slow_query_log = ON; +SET GLOBAL long_query_time = 1; +SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log'; +``` + +### 常见优化手段 + +| 问题 | 优化方案 | +|------|---------| +| 缺少索引 | 为 WHERE/ORDER BY 字段添加索引 | +| 全表扫描 | 使用 EXPLAIN 分析查询计划 | +| N+1 查询 | 使用 joinedload/subqueryload 预加载关联数据 | +| 大分页 | 使用游标分页或延迟关联 | +| SELECT * | 只查询需要的字段 | + +### 索引优化示例 + +```python +# 模型定义中添加索引 +class Article(BaseModel): + __tablename__ = DB_PREFIX + "article" + + title = Column(String(255), index=True) # 单列索引 + status = Column(Integer, index=True) + category_id = Column(Integer, index=True) + + # 复合索引(在 __table_args__ 中定义) + __table_args__ = ( + Index('idx_status_category', 'status', 'category_id'), + ) +``` + +### 分页查询优化 + +```python +# 避免大分页:使用游标分页 +# 差:OFFSET 100000 LIMIT 10(扫描大量数据) +# 好:WHERE id > last_id LIMIT 10(利用主键索引) + +def paginate_after_id(self, after_id: int, limit: int = 10): + """游标分页:基于上次最后一条记录的 ID""" + return self.db.query(self.model).filter( + self.model.id > after_id, + self._soft_col() == 0 + ).order_by(self.model.id.asc()).limit(limit).all() +``` + +## 异步与并发 + +### 同步/异步选择 + +```python +# 纯 DB CRUD:同步 def(Tornado 自动线程池执行) +def get_position_list(request): + return position_service.get_page(request) + +# 含 Redis/文件 IO:async def + await +async def get_captcha(request): + redis = await get_redis() + # ... +``` + +:::tip 禁止混用 +禁止在 `async def` 端点中直接调用同步 DB 查询(会阻塞事件循环)。同步 DB 操作要么用 `def` 端点(自动线程池),要么用 `asyncio.to_thread()` 包裹。 +::: + +## 总结 + +性能调优的核心路径:Tornado 多进程 利用多核 CPU、数据库连接池匹配并发需求、Redis 缓存减少 DB 压力、索引和查询优化降低慢查询。建议先用监控数据定位瓶颈,再针对性调优,每次只调整一个参数并观察效果。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume6/security-hardening.md b/DjangoAdmin_Tornado_EleVue/zh/volume6/security-hardening.md new file mode 100644 index 0000000..15d1efb --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume6/security-hardening.md @@ -0,0 +1,200 @@ +# 安全加固 + +## 概述 + +生产环境的安全加固是防止数据泄露和服务被攻击的关键措施。本文从应用配置、传输安全、访问控制、数据保护等维度介绍安全加固实践。 + +:::tip 安全原则 +- 最小权限:只开放必要的端口和权限 +- 纵深防御:多层安全措施叠加 +- 默认安全:配置默认值应为安全选项 +::: + +## 应用层安全 + +### 关闭 DEBUG 模式 + +```bash +# .env 生产环境配置 +TORNADO_DEBUG=False +``` + +:::warning 重要 +DEBUG 模式会暴露详细的错误堆栈、SQL 查询等敏感信息,生产环境必须关闭。 +::: + +### JWT 安全 + +```bash +# .env 配置 +JWT_SECRET=<随机生成的64位强密码> +JWT_EXPIRE_MINUTES=720 +``` + +```bash +# 生成强密码 +python -c "import secrets; print(secrets.token_urlsafe(48))" +``` + +:::tip JWT 安全要点 +1. `JWT_SECRET` 必须使用随机强密码,禁止使用默认值 +2. 不同环境使用不同的 `JWT_SECRET` +3. 定期轮换 `JWT_SECRET`(需同时使所有已发 token 失效) +4. token 过期时间不宜过长,建议 12 小时以内 +::: + +### 演示模式 + +```bash +# 演示环境:禁止写操作 +TORNADO_DEMO=True +``` + +演示模式下所有写操作(新增/编辑/删除/状态变更/导入/导出)都会被 `@check_demo` 装饰器拦截。 + +## CORS 配置 + +```python +# core/config/app.py 中配置 +CORS_ORIGINS = [ + "https://www.example.com", + "https://admin.example.com", +] +``` + +:::warning CORS 安全 +- 生产环境禁止使用 `allow_origins=["*"]`(允许任意来源) +- 仅列出信任的前端域名 +- 如需支持多子域,使用正则匹配而非通配符 +::: + +## .env 文件安全 + +### 文件权限 + +```bash +# 设置 .env 文件权限(仅运行用户可读写) +chmod 600 .env + +# 查看权限 +ls -la .env +# -rw------- 1 app app 1024 Sep 5 10:00 .env +``` + +### 敏感信息管理 + +```bash +# .env 中的敏感字段 +DB_PASS=<数据库密码> +REDIS_PASSWORD= +JWT_SECRET= +``` + +:::tip 安全建议 +1. `.env` 文件禁止提交到 Git(已在 `.gitignore` 中排除) +2. 不同环境的 `.env` 独立管理,禁止共用 +3. 生产环境密码至少 16 位,包含大小写字母、数字、特殊字符 +4. 定期轮换数据库密码和 JWT 密钥 +::: + +## 数据库安全 + +### MySQL 安全配置 + +```sql +-- 创建专用数据库用户(禁止使用 root) +CREATE USER 'djangoadmin'@'%' IDENTIFIED BY '<强密码>'; +GRANT SELECT, INSERT, UPDATE, DELETE ON djangoadmin_prod.* TO 'djangoadmin'@'%'; +FLUSH PRIVILEGES; + +-- 禁止远程 root 登录 +DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1'); +FLUSH PRIVILEGES; +``` + +:::tip 最小权限原则 +应用数据库用户只需 SELECT/INSERT/UPDATE/DELETE 权限,禁止 GRANT/CREATE/DROP 等管理权限。 +::: + +### Redis 安全 + +```bash +# redis.conf 安全配置 +# 设置密码 +requirepass <强密码> + +# 禁止危险命令 +rename-command FLUSHDB "" +rename-command FLUSHALL "" +rename-command CONFIG "" +rename-command DEBUG "" + +# 绑定地址 +bind 127.0.0.1 + +# 禁用 protected-mode(设置了密码后) +protected-mode no +``` + +## 防火墙配置 + +### iptables 基础规则 + +```bash +# 仅开放必要端口 +iptables -A INPUT -p tcp --dport 22 -j ACCEPT # SSH +iptables -A INPUT -p tcp --dport 80 -j ACCEPT # HTTP +iptables -A INPUT -p tcp --dport 443 -j ACCEPT # HTTPS +iptables -A INPUT -p tcp --dport 8041 -j ACCEPT # Tornado(如不走 Nginx) +iptables -A INPUT -i lo -j ACCEPT # 本地回环 +iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT +iptables -P INPUT DROP # 默认拒绝 + +# 保存规则 +iptables-save > /etc/iptables/rules.v4 +``` + +:::tip 端口规划 +| 端口 | 服务 | 是否对外 | 备注 | +|------|------|---------|------| +| 22 | SSH | 是(限制 IP) | 建议修改默认端口 | +| 80 | Nginx HTTP | 是 | 跳转 HTTPS | +| 443 | Nginx HTTPS | 是 | 主入口 | +| 8041 | Tornado | 否 | 仅 Nginx 可访问 | +| 3306 | MySQL | 否 | 仅应用可访问 | +| 6379 | Redis | 否 | 仅应用可访问 | +::: + +## 上传文件安全 + +```python +# 文件类型白名单 +ALLOWED_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.pdf', '.doc', '.docx', '.xls', '.xlsx'} + +# 文件大小限制 +MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB +``` + +:::tip 文件安全措施 +1. 上传文件重命名为 UUID,防止路径遍历攻击 +2. 验证文件类型(检查文件头,不依赖扩展名) +3. 上传目录禁止执行权限 +4. 图片文件重新编码(防止嵌入恶意代码) +::: + +## 安全检查清单 + +- [ ] `TORNADO_DEBUG=False` +- [ ] `JWT_SECRET` 使用随机强密码 +- [ ] `.env` 文件权限 `600` +- [ ] CORS 不使用 `*` 通配符 +- [ ] 数据库用户非 root 且最小权限 +- [ ] Redis 设置密码并禁用危险命令 +- [ ] 防火墙仅开放必要端口 +- [ ] 上传文件类型白名单验证 +- [ ] 日志不记录敏感信息(密码、token) +- [ ] HTTPS 启用且禁用 TLSv1.0/1.1 + +## 总结 + +安全加固的核心是纵深防御:应用层关闭 DEBUG、配置强 JWT 密钥、限制 CORS;数据层数据库最小权限、Redis 密码保护;网络层防火墙仅开放必要端口;文件层上传白名单验证。生产环境务必逐项检查安全清单,定期审计。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume6/supervisor.md b/DjangoAdmin_Tornado_EleVue/zh/volume6/supervisor.md new file mode 100644 index 0000000..a88e4e2 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume6/supervisor.md @@ -0,0 +1,195 @@ +# Supervisor进程管理 + +## 概述 + +Supervisor 是一个 Python 编写的进程管理工具,用于在 Linux 服务器上管理 Tornado 应用进程。它提供自动重启、日志管理、进程监控等功能,是生产环境部署的推荐方案之一。 + +:::tip 适用场景 +- Linux 服务器直接部署(非 Docker) +- 需要进程自动重启和日志轮转 +- 管理多个相关进程 +::: + +## 配置文件示例 + +项目提供了 Supervisor 配置模板 `deploy/supervisor.conf`,完整内容: + +```ini +; Supervisor 进程管理示例配置 +; 将 command / directory 替换为实际路径后复制到 /etc/supervisor/conf.d/ + +[program:djangoadmin-tornado] +; 启动命令:使用项目虚拟环境中的 tornado.httpserver +command=/path/to/venv/bin/tornado.httpserver src.main:app --host 127.0.0.1 --port 8041 +; 项目根目录 +directory=/data/apps/djangoadmin +; 自动启动 +autostart=true +; 异常退出后自动重启 +autorestart=true +; 停止时终止子进程 +stopasgroup=true +killasgroup=true +; 错误输出重定向到标准输出 +redirect_stderr=true +; 日志文件 +stdout_logfile=/path/to/logs/supervisor.out.log +; 日志轮转:单文件最大 50MB,保留 10 个备份 +stdout_logfile_maxbytes=50MB +stdout_logfile_backups=10 +; 环境变量 +environment=PYTHONUNBUFFERED=1 +``` + +:::tip 配置路径 +将修改后的配置文件复制到 `/etc/supervisor/conf.d/` 目录下,需修改两处路径: +```bash +# 1. 修改 command 中的虚拟环境路径 +# 2. 修改 directory 为项目实际路径 +# 3. 修改 stdout_logfile 为日志实际路径 +cp deploy/supervisor.conf /etc/supervisor/conf.d/djangoadmin-tornado.conf +``` +::: + +## 安装 Supervisor + +```bash +# Ubuntu/Debian +sudo apt-get install supervisor + +# CentOS/RHEL +sudo yum install supervisor + +# pip 安装 +pip install supervisor +``` + +## 常用命令 + +### 进程管理 + +```bash +# 读取配置并启动进程 +sudo supervisorctl reread +sudo supervisorctl update + +# 启动进程 +sudo supervisorctl start djangoadmin-tornado + +# 停止进程 +sudo supervisorctl stop djangoadmin-tornado + +# 重启进程 +sudo supervisorctl restart djangoadmin-tornado + +# 查看进程状态 +sudo supervisorctl status + +# 停止所有进程 +sudo supervisorctl stop all + +# 重启所有进程 +sudo supervisorctl restart all +``` + +### 服务管理 + +```bash +# 启动 Supervisor 服务 +sudo systemctl start supervisord + +# 停止 Supervisor 服务 +sudo systemctl stop supervisord + +# 设置开机自启 +sudo systemctl enable supervisord + +# 查看服务状态 +sudo systemctl status supervisord +``` + +## 日志轮转 + +Supervisor 内置日志轮转功能,通过以下配置控制: + +```ini +; 单个日志文件最大 50MB +stdout_logfile_maxbytes=50MB +; 保留 10 个历史日志文件 +stdout_logfile_backups=10 +``` + +:::tip 日志文件命名 +当 `stdout_logfile_maxbytes` 达到上限时,Supervisor 自动轮转: +- `supervisor.out.log`(当前日志) +- `supervisor.out.log.1`(上一个) +- `supervisor.out.log.2`(更早的) +- ...最多保留 `stdout_logfile_backups` 个 +::: + +## Web 管理界面 + +Supervisor 提供可选的 Web 管理界面: + +```ini +[inet_http_server] +port=127.0.0.1:9001 +username=admin +password=your_password +``` + +:::warning 安全提醒 +Web 界面仅监听 127.0.0.1,禁止暴露到公网。如需远程访问,应通过 SSH 隧道或 VPN。 +::: + +## 多进程管理 + +如需同时管理多个进程(如应用 + Celery Worker): + +```ini +[program:djangoadmin-tornado] +command=/path/to/venv/bin/tornado.httpserver src.main:app --host 127.0.0.1 --port 8041 +directory=/data/apps/djangoadmin +autostart=true +autorestart=true + +[program:djangoadmin-worker] +command=/path/to/venv/bin/celery -A tasks worker +directory=/data/apps/djangoadmin +autostart=true +autorestart=true +numprocs=4 +``` + +## 常见问题 + +### 进程启动失败 + +```bash +# 查看详细错误日志 +sudo supervisorctl tail -f djangoadmin-tornado + +# 检查配置语法 +sudo supervisorctl status +``` + +:::tip 常见原因 +1. Python 虚拟环境路径错误 +2. 项目目录路径错误 +3. 端口被占用 +4. `.env` 文件缺失或配置错误 +::: + +### 进程频繁重启 + +```bash +# 查看退出原因 +sudo supervisorctl tail djangoadmin-tornado stderr + +# 调整启动等待时间(给应用足够的启动时间) +startsecs=10 +``` + +## 总结 + +Supervisor 是 Linux 环境下管理 Tornado 进程的可靠方案,提供自动重启、日志轮转、进程监控等能力。配置文件位于 `deploy/supervisor.conf`,修改路径后复制到 `/etc/supervisor/conf.d/` 即可使用。生产环境建议配合 systemd 管理 Supervisor 自身的启动。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume6/troubleshooting.md b/DjangoAdmin_Tornado_EleVue/zh/volume6/troubleshooting.md new file mode 100644 index 0000000..ef2f583 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume6/troubleshooting.md @@ -0,0 +1,289 @@ +# 故障排查手册 + +## 概述 + +本文列出 常见故障及排查方案,帮助运维和开发人员快速定位和解决问题。 + +:::tip 排查思路 +1. 查看日志:`tail -f logs/supervisor.out.log` 或 `docker-compose logs -f` +2. 检查配置:`.env` 文件、数据库连接、Redis 连接 +3. 检查依赖:Python 包版本、系统服务状态 +4. 逐步排除:从最简单的可能性开始 +::: + +## 1. 启动失败:端口被占用 + +**现象**:启动时报 `OSError: [Errno 98] Address already in use` + +**排查**: + +```bash +# 查看端口占用 +lsof -i :8041 +# 或 +netstat -tlnp | grep 8041 + +# 杀掉占用进程 +kill -9 +``` + +:::tip 常见原因 +- 上次启动未正常退出,进程残留 +- 其他服务占用了 8041 端口 +- Supervisor 或 Docker 重复启动 +::: + +## 2. 启动失败:数据库连接失败 + +**现象**:启动时报 `OperationalError: (pymysql.err.OperationalError) (2003, "Can't connect to MySQL server")` + +**排查**: + +```bash +# 1. 检查数据库服务是否运行 +systemctl status mysql + +# 2. 检查网络连通性 +telnet 127.0.0.1 3306 + +# 3. 检查 .env 配置 +cat .env | grep DB_ + +# 4. 检查数据库用户权限 +mysql -u root -p -e "SELECT user, host FROM mysql.user;" +``` + +:::tip 常见原因 +- MySQL 服务未启动 +- `.env` 中 DB_HOST/DB_PORT 配置错误 +- 数据库用户密码错误 +- 防火墙阻止连接 +::: + +## 3. 启动失败:Redis 连接失败 + +**现象**:启动时报 `redis.exceptions.ConnectionError` + +**排查**: + +```bash +# 1. 检查 Redis 服务 +systemctl status redis + +# 2. 测试连接 +redis-cli -h 127.0.0.1 -p 6379 ping + +# 3. 检查 Redis 密码 +redis-cli -h 127.0.0.1 -p 6379 -a ping +``` + +## 4. 401 未认证错误 + +**现象**:API 返回 `{"code": 401, "msg": "未登录或 token 已过期"}` + +**排查**: + +```bash +# 1. 检查 token 是否正确传递 +curl -H "Authorization: Bearer " http://127.0.0.1:8041/api/v1/user/info + +# 2. 检查 token 是否过期(解码 JWT) +python -c " +import jwt +token = '' +payload = jwt.decode(token, options={'verify_signature': False}) +print(payload) +" + +# 3. 检查 Redis 中 token 黑名单 +redis-cli GET "token:blacklist:" +``` + +:::tip 常见原因 +- token 已过期(JWT_EXPIRE_MINUTES 到期) +- token 被加入黑名单(用户已退出登录) +- 前端未正确传递 Authorization 头 +- JWT_SECRET 配置变更导致旧 token 失效 +::: + +## 5. 403 无权限错误 + +**现象**:API 返回 `{"code": 403, "msg": "暂无权限访问"}` + +**排查**: + +```bash +# 1. 检查用户角色配置 +# 登录后台 → 系统管理 → 用户管理 → 查看用户角色 + +# 2. 检查角色权限配置 +# 系统管理 → 角色管理 → 查看角色菜单权限 + +# 3. 检查菜单权限标识 +# 系统管理 → 菜单管理 → 查看权限标识(如 sys:position:page) + +# 4. 清除权限缓存 +redis-cli KEYS "perm:*" | xargs redis-cli DEL +``` + +:::tip admin 用户 +用户 ID 为 1 的管理员账号跳过所有权限检查。排查权限问题时,先用 admin 账号确认功能正常。 +::: + +## 6. 验证码不显示或校验失败 + +**现象**:验证码图片加载失败或提交后提示验证码错误 + +**排查**: + +```bash +# 1. 检查验证码接口 +curl http://127.0.0.1:8041/api/v1/captcha -o captcha.png + +# 2. 检查 Redis 连接(验证码存储在 Redis) +redis-cli GET "captcha:" + +# 3. 检查 Pillow 依赖 +pip show Pillow +``` + +:::tip 常见原因 +- Pillow 未安装或版本不兼容 +- Redis 连接异常 +- 验证码过期(默认 5 分钟) +- 前端传递的验证码 UUID 不匹配 +::: + +## 7. 文件上传失败 + +**现象**:上传文件返回 500 错误或文件大小为 0 + +**排查**: + +```bash +# 1. 检查上传目录权限 +ls -la /data/apps/uploads/ +chmod 755 /data/apps/uploads/ + +# 2. 检查磁盘空间 +df -h + +# 3. 检查 Nginx 上传限制 +grep client_max_body_size /etc/nginx/nginx.conf + +# 4. 检查上传接口请求 +curl -X POST -F "file=@test.jpg" \ + -H "Authorization: Bearer " \ + http://127.0.0.1:8041/api/v1/upload/file +``` + +:::tip 常见原因 +- 上传目录不存在或无写权限 +- 磁盘空间不足 +- Nginx `client_max_body_size` 限制过小 +- 文件大小超过后端限制 +::: + +## 8. 慢查询导致接口超时 + +**现象**:列表接口响应时间超过 3 秒 + +**排查**: + +```bash +# 1. 开启 MySQL 慢查询日志 +mysql -u root -p -e " + SET GLOBAL slow_query_log = ON; + SET GLOBAL long_query_time = 1; +" + +# 2. 查看慢查询 +tail -f /var/log/mysql/slow.log + +# 3. 分析查询计划 +mysql -u root -p -e "EXPLAIN SELECT * FROM tornado_article WHERE status = 1;" + +# 4. 检查索引 +mysql -u root -p -e "SHOW INDEX FROM tornado_article;" +``` + +:::tip 优化建议 +- 为 WHERE/ORDER BY 字段添加索引 +- 避免 `SELECT *`,只查需要的字段 +- 大分页使用游标分页 +- 热点数据使用 Redis 缓存 +::: + +## 9. 内存持续增长(内存泄漏) + +**现象**:进程内存持续增长,最终 OOM + +**排查**: + +```bash +# 1. 监控内存变化 +watch -n 5 'ps aux | grep tornado.httpserver' + +# 2. 查看进程内存 +cat /proc//status | grep VmRSS + +# 3. 使用 memory_profiler 分析 +pip install memory_profiler +python -m memory_profiler app.py +``` + +:::tip 常见原因 +- 全局变量不断累积数据 +- 数据库连接未正确释放 +- 大文件读入内存未释放 +- 日志 Handler 积累 + +解决方案:使用 `--max-requests` 参数定期重启 Worker。 +::: + +## 10. Docker 容器启动失败 + +**现象**:`docker-compose up` 后容器立即退出 + +**排查**: + +```bash +# 1. 查看容器日志 +docker-compose logs tornado_elevue + +# 2. 进入容器调试 +docker run -it --rm tornado_elevue bash + +# 3. 检查 .env 文件 +cat .env + +# 4. 检查 Docker 网络 +docker network ls +docker network inspect host +``` + +:::tip 常见原因 +- `.env` 文件未正确挂载或配置错误 +- 容器内无法访问宿主机的 MySQL/Redis(host 网络模式需检查) +- 依赖安装失败(网络问题) +- Python 版本不兼容 +::: + +## 快速排查命令速查 + +| 场景 | 命令 | +|------|------| +| 查看服务日志 | `tail -f logs/supervisor.out.log` | +| 查看 Docker 日志 | `docker-compose logs -f` | +| 检查端口占用 | `lsof -i :8041` | +| 检查进程状态 | `ps aux \| grep tornado.httpserver` | +| 检查磁盘空间 | `df -h` | +| 检查内存使用 | `free -h` | +| 测试数据库连接 | `mysql -u root -p -e "SELECT 1"` | +| 测试 Redis 连接 | `redis-cli ping` | +| 检查 Nginx 配置 | `nginx -t` | +| 重启应用 | `supervisorctl restart djangoadmin-tornado` | + +## 总结 + +故障排查的核心思路:先看日志定位错误,再检查配置排除低级问题,最后逐步深入分析。本文覆盖了启动失败、认证授权、验证码、文件上传、慢查询、内存泄漏、Docker 等 10 类常见故障,每类均提供排查步骤和常见原因。建议收藏本文作为运维应急参考。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume6/upgrade.md b/DjangoAdmin_Tornado_EleVue/zh/volume6/upgrade.md new file mode 100644 index 0000000..83a4ac1 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume6/upgrade.md @@ -0,0 +1,224 @@ +# 版本升级流程 + +## 概述 + +本文介绍版本升级标准流程,包括升级前准备、执行步骤、验证检查和回滚方案。遵循规范的升级流程可最大程度降低线上风险。 + +:::tip 升级原则 +1. 先备份,后升级 +2. 先测试环境,后生产环境 +3. 每次升级可回滚 +4. 低峰期执行升级 +::: + +## 升级前准备 + +### 备份 + +```bash +# 1. 数据库全量备份 +mysqldump -h 127.0.0.1 -u root -p --single-transaction \ + djangoadmin_prod | gzip > backup_$(date +%Y%m%d_%H%M%S).sql.gz + +# 2. 上传文件备份 +tar -czf uploads_$(date +%Y%m%d).tar.gz /data/apps/uploads/ + +# 3. 应用目录备份(含 .env 和配置) +tar -czf app_$(date +%Y%m%d).tar.gz \ + --exclude='uploads' --exclude='__pycache__' --exclude='.git' \ + /data/apps/ +``` + +### 查看变更日志 + +```bash +# 查看当前版本 +git describe --tags --always + +# 查看提交记录 +git log --oneline -20 + +# 查看变更文件 +git diff --stat HEAD~10 +``` + +:::tip 重点关注 +- 是否有数据库结构变更(Alembic 迁移脚本) +- 是否有 `.env` 新增配置项 +- 是否有依赖包更新(requirements.txt) +- 是否有破坏性变更(Breaking Changes) +::: + +## 标准升级步骤 + +### 步骤一:拉取代码 + +```bash +cd /data/apps/djangoadmin + +# 拉取最新代码 +git fetch origin +git pull origin main + +# 或切换到指定版本 +git checkout v1.2.0 +``` + +### 步骤二:更新依赖 + +```bash +# 激活虚拟环境 +source venv/bin/activate + +# 更新依赖 +pip install -r requirements.txt +``` + +### 步骤三:更新配置 + +```bash +# 检查是否有新增的 .env 配置项 +git diff HEAD~1 .env.example + +# 如有新增项,手动添加到 .env +vim .env +``` + +### 步骤四:执行数据库迁移 + +```bash +# 查看待执行的迁移 +alembic current +alembic history --indicate-current + +# 执行迁移 +alembic upgrade head + +# 或使用 Make 命令 +make migrate-upgrade +``` + +:::warning 迁移前确认 +- 迁移脚本需提前在测试环境验证 +- 确认迁移不会锁表影响线上服务 +- 大表结构变更建议在低峰期执行 +::: + +### 步骤五:重启服务 + +```bash +# Supervisor 管理 +sudo supervisorctl restart djangoadmin-tornado + +# Docker 部署 +docker-compose down && docker-compose up --build -d + +# Systemd 管理 +sudo systemctl restart djangoadmin-tornado +``` + +### 步骤六:验证 + +```bash +# 1. 检查服务状态 +sudo supervisorctl status djangoadmin-tornado + +# 2. 检查健康端点 +curl -s http://127.0.0.1:8041/api/v1/health | python -m json.tool + +# 3. 检查日志 +tail -f /data/apps/logs/supervisor.out.log + +# 4. 验证核心功能 +# - 登录接口 +# - 列表查询接口 +# - 新增/编辑/删除接口 +``` + +## 回滚方案 + +### 代码回滚 + +```bash +# 回退到上一个版本 +git checkout HEAD~1 + +# 或回退到指定版本 +git checkout v1.1.0 + +# 重新安装依赖 +pip install -r requirements.txt + +# 重启服务 +sudo supervisorctl restart djangoadmin-tornado +``` + +### 数据库回滚 + +```bash +# 回滚最近一次迁移 +alembic downgrade -1 + +# 回滚到指定版本 +alembic downgrade + +# 查看可用版本 +alembic history +``` + +### 完整回滚 + +```bash +# 1. 停止服务 +sudo supervisorctl stop djangoadmin-tornado + +# 2. 恢复应用目录 +cd /data/apps/ +tar -xzf app_20260905.tar.gz + +# 3. 恢复数据库 +gunzip < backup_20260905.sql.gz | mysql -u root -p djangoadmin_prod + +# 4. 恢复上传文件 +tar -xzf uploads_20260905.tar.gz -C / + +# 5. 启动服务 +sudo supervisorctl start djangoadmin-tornado +``` + +## 灰度升级策略 + +对于重要生产环境,建议采用灰度升级: + +``` +1. 部署新版本到 1 台服务器 +2. Nginx 将 10% 流量切到新版本 +3. 观察 30 分钟,无异常后逐步增加流量 +4. 流量 100% 切到新版本 +5. 下线旧版本 +``` + +```nginx +# Nginx 灰度配置 +upstream backend { + server 10.0.1.10:8041 weight=9; # 旧版本 90% + server 10.0.1.11:8041 weight=1; # 新版本 10% +} +``` + +## 升级检查清单 + +- [ ] 升级前已完成数据库备份 +- [ ] 升级前已完成应用目录备份 +- [ ] 变更日志已审查,了解所有变更 +- [ ] 新增 `.env` 配置项已添加 +- [ ] 数据库迁移已在测试环境验证 +- [ ] 依赖包已更新 +- [ ] 服务已重启 +- [ ] 健康检查通过 +- [ ] 核心功能验证通过 +- [ ] 日志无异常报错 + +## 总结 + +版本升级的核心流程:备份 → 拉代码 → 更新依赖 → 更新配置 → 数据库迁移 → 重启 → 验证。每一步都需要可回滚,完整回滚方案应提前准备并验证。生产环境建议在低峰期执行,重要系统采用灰度升级策略。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume7/api-response.md b/DjangoAdmin_Tornado_EleVue/zh/volume7/api-response.md new file mode 100644 index 0000000..775b6f9 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume7/api-response.md @@ -0,0 +1,162 @@ +# API响应规范 + +## 概述 + + 统一使用 `R` 模块封装 API 响应,所有接口返回一致的 JSON 格式,便于前端统一处理。 + +:::tip 核心原则 +- 所有接口必须通过 `R.ok()` 或 `R.failed()` 返回,禁止直接返回 dict +- 成功响应 `code=0`,失败响应 `code=1` +- 响应体始终包含 `code`、`data`、`msg`、`ok` 四个字段 +::: + +## 响应格式 + +### 成功响应 + +```json +{ + "code": 0, + "data": { ... }, + "msg": "操作成功", + "ok": true +} +``` + +### 失败响应 + +```json +{ + "code": 1, + "data": null, + "msg": "错误信息", + "ok": false +} +``` + +### 分页响应 + +```json +{ + "code": 0, + "data": { + "records": [ ... ], + "total": 100, + "size": 10, + "current": 1, + "pages": 10 + }, + "msg": "操作成功", + "ok": true +} +``` + +## R 模块使用方法 + +### R.ok() 成功响应 + +```python +from core import R + +# 基础成功响应 +return R.ok() + +# 带数据的成功响应 +return R.ok(data=user_info) + +# 自定义提示消息 +return R.ok(msg='添加成功') + +# 附加额外字段(如 count) +return R.ok(data=result, count=total) +``` + +### R.failed() 失败响应 + +```python +# 基础失败响应 +return R.failed() + +# 自定义错误消息 +return R.failed('岗位名称不能重复') + +# 自定义错误码和消息 +return R.failed(msg='未授权', code=401) +``` + +### R.page() 分页响应 + +```python +# 分页响应(由 BaseService.get_page 自动调用) +return R.page(data=records, total=100, current=1, size=10) +``` + +## 字段定义 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `code` | int | 状态码:0=成功,1=失败,401=未认证,403=无权限 | +| `data` | any/null | 响应数据,失败时为 null | +| `msg` | string | 提示消息 | +| `ok` | boolean | 是否成功 | + +## 状态码规范 + +| 状态码 | 含义 | 使用场景 | +|--------|------|---------| +| 0 | 成功 | 操作成功 | +| 1 | 失败 | 业务逻辑失败(如参数校验、唯一性冲突) | +| 401 | 未认证 | token 缺失或过期 | +| 403 | 无权限 | 用户无对应权限 | +| 404 | 资源不存在 | 请求的资源未找到 | +| 422 | 验证错误 | Pydantic 参数校验失败 | +| 500 | 服务器错误 | 未捕获的异常 | + +:::tip 错误码与 HTTP 状态码 +业务错误码(code 字段)始终为 0 或 1,HTTP 状态码始终为 200。401/403/422 等仅在全局异常处理器中使用,常规业务错误统一返回 `code=1`。 +::: + +## 在 Endpoint 中使用 + +```python +from core import response as R + +# 查询详情:service 返回 dict,Handler 包 R.ok +class PositionDetailHandler(BaseHandler): + async def get(self, id): + result = position_service.get_detail(id) + return R.ok(self, data=result) + +# 添加记录:service 直接返回 R 对象 +class PositionAddHandler(BaseHandler): + async def post(self): + return await position_service.add(self) + +# 删除记录:service 直接返回 R 对象 +class PositionDeleteHandler(BaseHandler): + async def delete(self, id): + return await position_service.delete(self, id) +``` + +:::tip 响应封装层级 +- **分页/列表查询**:service 返回 `R.page()` +- **详情查询**:service 返回 dict/None,Handler 包 `R.ok(self, data=result)` +- **写操作(增/删/改/状态)**:service 直接返回 `R` 对象 +::: + +## Pydantic 验证错误响应 + +Pydantic 验证错误由全局 `RequestValidationError` 处理器自动转换为标准格式: + +```json +{ + "code": 1, + "data": null, + "msg": "name: 字段不能为空 / status: 输入值超出范围", + "ok": false +} +``` + +## 总结 + +API 响应规范通过 `R` 模块统一封装:`R.ok()` 返回成功、`R.failed()` 返回失败、`R.page()` 返回分页。所有响应保持 `{code, data, msg, ok}` 四字段结构,HTTP 状态码始终为 200。业务层错误统一用 `code=1`,前端只需判断 `ok` 字段即可。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume7/code-review.md b/DjangoAdmin_Tornado_EleVue/zh/volume7/code-review.md new file mode 100644 index 0000000..52085fa --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume7/code-review.md @@ -0,0 +1,109 @@ +# 代码审查清单 + +## 概述 + +代码审查(Code Review)是保障代码质量的重要环节。本文提供标准化的审查清单,覆盖安全、性能、规范、边界等维度,帮助审查者系统性地检查代码质量。 + +:::tip 审查原则 +- 每次提交必须经过至少一人审查 +- 审查关注点:正确性 > 安全性 > 可维护性 > 性能 +- 审查意见应具体、可操作,避免主观评价 +::: + +## 安全检查 + +- [ ] **DEBUG 关闭**:生产配置 `TORNADO_DEBUG=False` +- [ ] **JWT 安全**:`JWT_SECRET` 使用强密码,未硬编码在代码中 +- [ ] **CORS 配置**:未使用 `allow_origins=["*"]`,仅列出信任域名 +- [ ] **SQL 注入**:使用 ORM 参数化查询,禁止字符串拼接 SQL +- [ ] **权限校验**:写操作接口有 `@permission_required` 和 `@check_demo` 装饰器 +- [ ] **敏感信息**:日志中不记录密码、token、密钥等敏感信息 +- [ ] **文件上传**:有文件类型白名单验证,文件名已重命名 +- [ ] **输入校验**:所有用户输入通过 Pydantic Schema 校验 +- [ ] **.env 安全**:`.env` 文件未提交到 Git(在 `.gitignore` 中) +- [ ] **密码存储**:使用 bcrypt 哈希存储,禁止明文 + +## 性能检查 + +- [ ] **N+1 查询**:关联查询使用 joinedload/subqueryload,避免循环查询 +- [ ] **索引覆盖**:WHERE/ORDER BY 字段已添加索引 +- [ ] **分页查询**:大列表接口使用分页,禁止全表查询返回 +- [ ] **SELECT ***:只查询需要的字段,避免 `SELECT *` +- [ ] **缓存使用**:热点数据使用 Redis 缓存 +- [ ] **同步/异步**:`async def` 端点中未直接调用同步 DB 查询 +- [ ] **批量操作**:批量插入/更新使用 `executemany`,非逐条执行 +- [ ] **连接池**:数据库连接池大小合理,不超过数据库 max_connections + +## 代码规范检查 + +- [ ] **装饰器顺序**:`@router` → `@permission_required` → `@check_demo` +- [ ] **命名规范**:文件名 snake_case,类名 PascalCase,函数名 snake_case +- [ ] **注释规范**:关键逻辑有注释说明,docstring 描述函数用途 +- [ ] **导入规范**:导入分组(标准库 → 第三方 → 本地模块),无未使用的导入 +- [ ] **R 封装**:所有接口通过 `R.ok()`/`R.failed()` 返回,禁止直接返回 dict +- [ ] **删除规范**:使用 `delete` 方法,禁止直接调用 `repo.batch_delete` +- [ ] **Repository 单例**:模块级单例模式(`xxx_repo = XxxRepository(Model)`) +- [ ] **Service 单例**:模块级单例模式(`xxx_service = XxxService()`) + +## 边界条件检查 + +- [ ] **空值处理**:可选字段支持 None 值,不抛异常 +- [ ] **空列表**:批量操作传入空列表时安全返回 +- [ ] **超大输入**:字符串长度限制合理(`max_length`) +- [ ] **数值范围**:数值字段有 `ge`/`le`/`gt`/`lt` 约束 +- [ ] **ID 有效性**:ID 参数校验 > 0,兼容字符串 ID +- [ ] **重复操作**:重复添加/删除同一数据的幂等性 +- [ ] **并发安全**:并发修改同一记录的处理(乐观锁/唯一性校验) +- [ ] **软删除过滤**:查询方法自动过滤 `is_delete = 0` + +## 数据库检查 + +- [ ] **模型定义**:继承 `base_model` 和 `BaseModel` +- [ ] **表名前缀**:`__tablename__ = DB_PREFIX + "表名"` +- [ ] **字段注释**:`comment` 参数描述字段用途 +- [ ] **索引定义**:常用查询字段有索引 +- [ ] **默认值**:`server_default` 与数据库默认值一致 +- [ ] **软删除**:不使用物理删除,统一软删除 + +## API 接口检查 + +- [ ] **HTTP 方法**:查询用 GET,新增用 POST,编辑用 PUT,删除用 DELETE +- [ ] **权限标识**:格式 `sys:{module}:{action}` +- [ ] **请求参数**:Pydantic Schema 定义完整约束 +- [ ] **响应格式**:统一使用 `R.ok()`/`R.failed()`/`R.page()` +- [ ] **错误提示**:错误消息清晰、用户友好 +- [ ] **路由注册**:新模块已在 `api/v1/router.py` 中注册 + +## 前端检查 + +- [ ] **API 函数命名**:`getXxxPage`/`addXxx`/`updateXxx`/`deleteXxx` +- [ ] **错误处理**:响应拦截器统一处理错误 +- [ ] **loading 状态**:数据加载时显示 loading +- [ ] **确认弹窗**:删除操作有二次确认 +- [ ] **表单校验**:提交前前端校验必填项 + +## 提交检查 + +- [ ] **提交信息**:遵循 Conventional Commits 格式 +- [ ] **变更范围**:每次提交只做一件事 +- [ ] **测试覆盖**:新功能/修复有对应测试 +- [ ] **文档更新**:接口变更同步更新文档 + +## 审查意见模板 + +``` +[严重] 安全问题:SQL 拼接存在注入风险,应使用 ORM 参数化查询 +[建议] 性能优化:此处可用 joinedload 避免 N+1 查询 +[疑问] 这里的 try-except 吞掉了异常,是否需要记录日志? +[肯定] 使用 delete 统一封装删除响应,符合规范 +``` + +:::tip 审查态度 +- 区分「必须修改」和「建议优化」 +- 给出具体的修改方案,而非只指出问题 +- 对好的实践给予肯定 +::: + +## 总结 + +代码审查清单覆盖安全、性能、规范、边界、数据库、API、前端、提交等 8 个维度。审查时按清单逐项检查,区分严重问题和建议优化。核心关注点:权限校验是否完整、SQL 是否安全、同步/异步是否正确、响应格式是否统一。审查意见应具体可操作,帮助团队持续提升代码质量。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume7/deletion.md b/DjangoAdmin_Tornado_EleVue/zh/volume7/deletion.md new file mode 100644 index 0000000..02f431e --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume7/deletion.md @@ -0,0 +1,165 @@ +# 删除规范 + +## 概述 + + 统一使用软删除机制,通过 `is_delete` 字段标记记录状态,不物理删除数据。删除操作分为单条删除和批量删除两种方式。 + +:::tip 软删除约定 +- `is_delete = 0`:正常记录(默认值) +- `is_delete = 1`:已删除记录 +- 所有查询方法自动过滤 `is_delete = 0`,业务层无需关心 +::: + +## 单条删除 + +### URL 格式 + +``` +DELETE /api/v1/{module}/delete/{id} +``` + +支持逗号分隔的多 ID 删除: + +``` +DELETE /api/v1/position/delete/1,2,3 +``` + +### Handler 示例 + +```python +class PositionDeleteHandler(BaseHandler): + @permission_required("sys:position:delete") + @check_demo + @operation_log("岗位管理", "删除") + async def delete(self, id): + """根据ID删除单个岗位""" + return await position_service.delete(self, id) +``` + +:::tip 装饰器顺序 +删除接口的装饰器顺序:`@permission_required` → `@check_demo` → `@operation_log` +::: + +## 批量删除 + +### 请求格式 + +``` +POST /api/v1/{module}/batchDelete +Content-Type: application/json + +[1, 2, 3] +``` + +### Handler 示例 + +```python +class PositionBatchDeleteHandler(BaseHandler): + @permission_required("sys:position:delete") + @check_demo + @operation_log("岗位管理", "批量删除") + async def post(self): + """批量删除岗位""" + return await position_service.batch_delete(self) +``` + +## 删除流程 + +### 原理 + +Repository 层的 `batch_delete` 只做数据原语(软删除 + 返回条数),Service 层负责 R 响应封装和文案统一。 + +```python +async def delete(self, handler, ids) -> R: + try: + id_list = list(dict.fromkeys(parse_id_list(str(ids)))) + if not id_list: + return R.failed(handler, "记录ID不存在") + if len(self.repo.get_by_ids(id_list)) != len(id_list): + return R.failed(handler, "记录不存在") + err = self._before_delete(ids) + if err: + return R.failed(handler, err) + count = self.repo.batch_delete(ids) + return R.ok(handler, msg="本次共删除{0}条数据".format(count)) + finally: + self.repo.close() +``` + +### 自定义 Service 扩展 + +```python +class PositionService(BaseService): + def _before_delete(self, ids) -> Optional[str]: + """删除前校验:存在用户引用时禁止删除""" + id_list = parse_id_list(str(ids)) + if id_list and user_repo.filter( + User.position_id.in_(id_list), User.is_delete == 0 + ).first(): + return "存在用户引用该岗位,请先调整用户岗位" + return None +``` + +:::tip _before_delete 钩子 +- 返回 `None`:放行,继续删除 +- 返回字符串:拦截,返回 `R.failed(字符串)` +- 用于检查子级引用、业务约束等 +::: + +## 删除流程 + +``` +1. Endpoint 接收请求 + ↓ +2. Service.delete(ids) 或 Service.batch_delete(request) + ↓ +3. _before_delete(ids) 钩子校验 + ↓(拦截则返回 R.failed) +4. delete(handler, ids) + ↓ +5. repo.batch_delete(ids) 软删除 + ↓ +6. 返回 R.ok(msg="本次共删除N条数据") +``` + +## Repository 层实现 + +```python +def batch_delete(self, ids_str) -> int: + """按ID软删除,返回实际删除条数""" + id_list = parse_id_list(str(ids_str)) + if not id_list: + return 0 + + records = self.db.query(self.model).filter( + self.model.id.in_(id_list), self._soft_col() == 0 + ).all() + + for record in records: + setattr(record, self.soft_delete_col, 1) + + return len(records) +``` + +:::tip 返回值说明 +`batch_delete` 返回实际软删除的条数。如果传入的 ID 中包含已删除或不存在的记录,返回值会小于入参数量,此时 `delete` 会返回失败提示。 +::: + +## 权限标识 + +| 操作 | 权限标识 | 说明 | +|------|---------|------| +| 单条删除 | `sys:{module}:delete` | 删除单条记录 | +| 批量删除 | `sys:{module}:batchDelete` | 批量删除记录 | + +## 常见错误提示 + +| 提示 | 原因 | +|------|------| +| "记录ID不存在" | 入参为空或格式错误 | +| "记录不存在" | 部分或全部 ID 对应的记录已删除/不存在 | +| "存在用户引用该岗位" | _before_delete 钩子拦截 | + +## 总结 + +删除规范统一使用软删除(`is_delete` 标记),支持单条删除(逗号分隔 ID)和批量删除(JSON 数组)。Repository 层做数据原语,Service 层通过 `delete` 统一封装响应。自定义 Service 通过 `_before_delete` 钩子添加业务校验。批量删除端点必须为 `async def`。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume7/doc-standard.md b/DjangoAdmin_Tornado_EleVue/zh/volume7/doc-standard.md new file mode 100644 index 0000000..b491686 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume7/doc-standard.md @@ -0,0 +1,175 @@ +# 文档规范 + +## 概述 + +良好的文档是项目可维护性的基石。本文规范 API 文档、Pydantic 文档、代码注释等文档的编写标准。 + +:::tip 文档原则 +- 文档与代码同步更新 +- 使用中文编写(项目约定) +- 示例代码可直接运行 +::: + +## API 接口文档 + +Tornado 自动生成 Swagger 文档,访问地址: + +``` +http://127.0.0.1:8041/docs # Swagger UI +http://127.0.0.1:8041/redoc # ReDoc +http://127.0.0.1:8041/openapi.json # OpenAPI JSON +``` + +### Handler 文档注释 + +```python +class PositionAddHandler(BaseHandler): + @permission_required("sys:position:add") + @check_demo + @operation_log("岗位管理", "添加") + async def post(self): + """新增岗位 + + - **name**: 岗位名称(必填,1-150字符) + - **status**: 岗位状态(必填,1-在用 2-停用) + - **sort**: 岗位排序(必填,0-99999) + """ + return await position_service.add(self) + """ + return await position_service.add(request, data) +``` + +:::tip 文档层级 +- `summary`:简短标题(显示在接口列表) +- docstring:详细描述(显示在接口详情) +- Pydantic `Field(description=...)`:字段级描述 +::: + +## Pydantic 文档 + +Pydantic 模型通过 `Field` 的 `description` 参数自动生成字段文档: + +```python +class PositionForm(BaseSchema): + """岗位创建/编辑表单""" + + name: str = Field( + ..., + min_length=1, + max_length=150, + description="岗位名称" + ) + status: int = Field( + ..., + ge=1, + le=2, + description="岗位状态:1-在用 2-停用" + ) + sort: int = Field( + ..., + ge=0, + le=99999, + description="岗位排序" + ) +``` + +:::tip description 规范 +- 必须填写 `description`,用于 Swagger 文档展示 +- 枚举值说明使用 `值-含义` 格式(如 `1-在用 2-停用`) +- 有约束条件时在描述中说明(如 `1-150字符`) +::: + +## 代码注释规范 + +### 文件头注释 + +```python +# +====================================================================== +# | 模块: 岗位业务逻辑层 +# | 说明: 岗位的增删改查、唯一性校验、状态管理等业务处理 +# +====================================================================== +``` + +### 类注释 + +```python +class PositionService(BaseService(Position]): + """岗位业务服务类,继承基础服务获得通用 CRUD 能力""" +``` + +### 方法注释 + +```python +def _before_delete(self, ids) -> Optional[str]: + """删除前校验:存在用户引用该岗位时禁止删除,避免用户岗位悬空 + + Args: + ids: 逗号分隔的ID字符串 + + Returns: + None: 放行删除 + str: 拦截删除,返回错误提示 + """ +``` + +### 行内注释 + +```python +# 自动软删过滤,调用方无需关心 is_delete +query = query.filter(self._soft_col() == 0) +``` + +:::tip 注释原则 +- 解释"为什么"而非"是什么" +- 复杂逻辑必须注释 +- 简单代码无需注释 +- 注释与代码同步更新 +::: + +## 文档目录结构 + +``` +document/ # 补充文档 +├── 数据库迁移_runbook.md # 数据库迁移操作手册 +└── djangoadmin.tornado.elevue2.sql # 数据库结构 SQL + +wiki/ # VitePress 文档站 +├── zh/ # 中文文档 +│ ├── 1. 了解项目/ # why.md, struct.md, course.md +│ ├── 2. 快速入门/ # volume1/(环境、启动、数据库、Docker) +│ ├── 3. 模块开发实战/ # volume2/ +│ ├── 4. 架构设计/ # volume3/ +│ ├── 5. 开发指南/ # volume4/ +│ │ ├── 5.1 后端核心功能/ # core/ +│ │ ├── 5.2 后端业务模块/ # business/ +│ │ ├── 5.3 后端通用工具/ # tools/ +│ │ ├── 5.4 前端开发/ # frontend/ +│ │ └── 5.5 前后端联调/ # integration/ +│ ├── 6. 代码生成器/ # volume5/ +│ ├── 7. 运维部署/ # volume6/ +│ └── 8. 规范标准/ # volume7/ +``` + +## 前端文档结构 + +前端相关文档分布在以下位置: + +| 文档 | 路径 | 说明 | +|------|------|------| +| 前端页面规范 | `volume7/fe-component.md` | 页面结构、组件使用、命名规范 | +| 前端 API 规范 | `volume7/fe-api.md` | API 文件组织、函数命名、请求规范 | +| 公共组件库 | `volume4/frontend/components.md` | 组件列表、Props、Events、用法 | +| 前端构建 | `volume4/frontend/build.md` | Vite 配置、环境变量、Nginx 部署 | +| API 层开发 | `volume4/frontend/api.md` | API 层开发指南 | +| 页面视图开发 | `volume4/frontend/view.md` | 页面视图开发指南 | + +## 文档更新检查清单 + +- [ ] 新增/修改 API 接口时,同步更新 Swagger 注释 +- [ ] 新增/修改 Pydantic 字段时,填写 `description` +- [ ] 复杂逻辑添加代码注释 +- [ ] 重大变更更新 wiki 文档 + +## 总结 + +文档规范覆盖 API 文档(Swagger 自动注释)、Pydantic 文档(Field description)、代码注释(文件头/类/方法/行内)。核心原则:文档与代码同步更新,使用中文编写,示例可直接运行。重大变更同步更新 wiki 文档。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume7/fe-api.md b/DjangoAdmin_Tornado_EleVue/zh/volume7/fe-api.md new file mode 100644 index 0000000..f6d28c0 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume7/fe-api.md @@ -0,0 +1,305 @@ +# 前端 API 规范 + +## 概述 + +前端 API 层封装所有 HTTP 请求,与后端接口一一对应。API 文件位于 `ui/src/api/` 目录,按模块分组,使用封装后的 `VAxios` 发送请求。 + +:::tip 设计原则 +- 每个后端模块对应一个 API 文件 +- 函数命名统一使用 `名词 + 动词` 格式(与后端保持一致) +- 请求路径使用相对路径(`/position/page`),不含 `/api/v1` 前缀(由 Axios baseURL 统一拼接) +- 统一通过 `http.request()` 发送请求 +::: + +## 目录结构 + +``` +ui/src/api/ +├── common/ # 公共 API(上传、字典按编码查询、城市、模板下载) +│ ├── index.ts +│ ├── menu.ts +│ ├── user.ts +│ └── city.ts +├── system/ # 系统模块 +│ ├── user.ts +│ ├── role.ts +│ ├── menu.ts +│ ├── dept.ts +│ ├── position.ts +│ ├── level.ts +│ ├── dictionary.ts +│ ├── loginLog.ts +│ ├── operLog.ts +│ └── tenant.ts +├── data/ # 数据模块 +│ ├── param.ts +│ ├── config.ts +│ ├── dictionary.ts +│ ├── notice.ts +│ └── city.ts +├── content/ # 内容模块 +│ ├── article.ts +│ ├── category.ts +│ └── link.ts +├── monitor/ # 监控模块 +│ └── job.ts +├── file/ # 文件模块 +│ └── fileTemplate.ts +├── tool/ # 工具模块 +│ ├── generator.ts +│ └── example.ts +├── setting/ # 设置模块 +│ ├── profile.ts +│ └── web.ts +├── region/ # 区域模块 +│ └── region.ts +└── dashboard/ # 仪表盘 + ├── console.ts + └── message.ts +``` + +## 函数命名规范 + +| 操作 | 命名格式 | 示例 | HTTP 方法 | +|------|---------|------|----------| +| 分页查询 | `getXxxList` 或 `getXxxPage` | `getPositionList` | GET | +| 全量列表 | `getXxxAllList` | `getPositionAllList` | GET | +| 详情查询 | `getXxxDetail` | `getPositionDetail` | GET | +| 添加 | `xxxAdd` | `positionAdd` | POST | +| 编辑 | `xxxUpdate` | `positionUpdate` | PUT | +| 删除 | `xxxDelete` | `positionDelete` | DELETE | +| 批量删除 | `xxxBatchDelete` | `positionBatchDelete` | DELETE | +| 导入 | `xxxImport` | `levelImport` | POST | +| 导出 | `xxxExport` | `levelExport` | GET | + +:::warning 命名风格 +实际代码中采用 `positionAdd` 而非 `addPosition`(名词在前、动词在后),与后端 Python 函数命名风格一致。 +::: + +## API 文件标准模板 + +以岗位管理(`api/system/position.ts`)为标准: + +```typescript +import { http } from '@/utils/http/axios'; + +/** + * @description: 岗位列表(分页) + */ +export function getPositionList(params?) { + return http.request({ + url: '/position/page', + method: 'GET', + params, + }); +} + +/** + * 获取全部岗位列表(不分页) + * @param params 参数 + * @returns 返回结果 + */ +export function getPositionAllList(params?) { + return http.request({ + url: '/position/list', + method: 'GET', + params, + }); +} + +/** + * @description: 根据ID获取详情 + */ +export function getPositionDetail(positionId) { + return http.request({ + url: '/position/detail/' + positionId, + method: 'get', + }); +} + +/** + * @description: 添加岗位 + */ +export function positionAdd(data: any) { + return http.request({ + url: '/position/add', + method: 'POST', + data, + }); +} + +/** + * @description: 更新岗位 + */ +export function positionUpdate(data: any) { + return http.request({ + url: '/position/update', + method: 'PUT', + data, + }); +} + +/** + * @description: 删除岗位 + */ +export function positionDelete(positionId) { + return http.request({ + url: '/position/delete/' + positionId, + method: 'DELETE', + }); +} + +/** + * @description: 批量删除岗位 + */ +export function positionBatchDelete(data: any) { + return http.request({ + url: '/position/batchDelete', + method: 'DELETE', + data, + }); +} +``` + +## 完整示例:用户模块 + +```typescript +// api/system/user.ts +import { http } from '@/utils/http/axios'; + +export interface BasicResponseModel { + code: number; + msg: string; + data: T; +} + +export interface BasicPageParams { + pageNo: number; + pageSize: number; + total: number; +} + +/** @description: 获取用户列表 */ +export function getUserList(params) { + return http.request({ url: '/user/page', method: 'get', params }); +} + +/** @description: 根据ID获取详情 */ +export function getUserDetail(userId) { + return http.request({ url: '/user/detail/' + userId, method: 'get' }); +} + +/** @description: 添加用户 */ +export function userAdd(data: any) { + return http.request({ url: '/user/add', method: 'POST', data }); +} + +/** @description: 更新用户 */ +export function userUpdate(data: any) { + return http.request({ url: '/user/update', method: 'PUT', data }); +} + +/** @description: 删除用户 */ +export function userDelete(userId) { + return http.request({ url: '/user/delete/' + userId, method: 'DELETE' }); +} + +/** @description: 批量删除用户 */ +export function userBatchDelete(data: any) { + return http.request({ url: '/user/batchDelete', method: 'DELETE', data }); +} + +/** @description: 重置密码 */ +export function resetPwd(data: any) { + return http.request({ url: '/user/resetPwd', method: 'PUT', data }); +} + +/** @description: 导出用户 */ +export function userExport(params) { + return http.request({ url: '/user/export', method: 'GET' }); +} +``` + +## 请求路径规则 + +URL 使用相对路径,不含 `/api/v1` 前缀(由 Axios 实例的 `baseURL` 统一拼接): + +| 后端路由 | 前端 URL | 说明 | +|----------|----------|------| +| `GET /api/v1/position/page` | `/position/page` | baseURL 已含 `/api/v1` | +| `GET /api/v1/position/detail/{id}` | `/position/detail/` + id | 路径参数拼接 | +| `POST /api/v1/position/add` | `/position/add` | - | +| `PUT /api/v1/position/update` | `/position/update` | - | +| `DELETE /api/v1/position/delete/{id}` | `/position/delete/` + id | - | +| `DELETE /api/v1/position/batchDelete` | `/position/batchDelete` | body 传 ID 数组 | + +## params 与 data 区别 + +| 参数 | 适用方法 | 说明 | +|------|----------|------| +| `params` | GET | query 参数,拼接到 URL 后面 | +| `data` | POST / PUT / DELETE | body 参数,放在请求体中 | + +```typescript +// GET 请求:params 拼到 URL +http.request({ url: '/position/page', method: 'GET', params: { pageNo: 1, pageSize: 10 } }); +// 实际请求: /api/v1/position/page?pageNo=1&pageSize=10 + +// POST 请求:data 放 body +http.request({ url: '/position/add', method: 'POST', data: { name: '测试', status: 1 } }); +// 实际请求: /api/v1/position/add body: {"name":"测试","status":1} +``` + +## 公共 API + +公共 API 位于 `api/common/index.ts`,包含跨模块复用的接口: + +```typescript +import { http } from '@/utils/http/axios'; + +/** 本地文件上传 */ +export function upload(data) { + return http.request({ url: '/upload/uploadFile', method: 'post', data }); +} + +/** 根据字典编码查询字典项 */ +export function getDictItemByCode(code) { + return http.request({ url: '/dict/item/getDictItemList/' + code, method: 'GET' }); +} + +/** 下载模板 */ +export function getTemplateByCode(code: any) { + return http.request({ url: '/file/template/getTemplateByCode/' + code, method: 'GET' }); +} +``` + +## 响应格式 + +后端统一返回 `{code, data, msg, ok}` 格式: + +```json +{ + "code": 0, + "data": { "records": [...], "total": 100 }, + "msg": "操作成功", + "ok": true +} +``` + +前端 Axios 拦截器自动处理: +- `ok: true` → 返回 `res.data` +- `ok: false` → 弹出 `res.msg` 错误提示 +- `code: 401` → token 失效,跳转登录页 + +页面中无需手动判断,直接使用返回值: + +```typescript +const loadDataTable = async (res: any) => { + const result = await getPositionList({ ...formParams, ...res }); + return result; // BasicTable 自动处理分页数据 +}; +``` + +## 总结 + +前端 API 规范:每个后端模块对应一个 API 文件,函数命名使用 `名词+动词` 格式(`positionAdd` / `positionDelete`)。URL 使用相对路径(不含 `/api/v1` 前缀),GET 用 `params`,POST/PUT/DELETE 用 `data`。统一通过 `http.request()` 发送请求,响应拦截器自动处理错误。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/volume7/fe-component.md b/DjangoAdmin_Tornado_EleVue/zh/volume7/fe-component.md new file mode 100644 index 0000000..17b8837 --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/volume7/fe-component.md @@ -0,0 +1,391 @@ +# 前端页面规范 + +## 概述 + +前端基于 Vue3 + ElementPlus + 自定义组件库构建。页面统一使用 `PageWrapper` + `BasicForm` + `BasicTable` + `TableAction` 四件套,分页由 `BasicTable` 内置处理,无需单独引入分页组件。 + +:::tip 技术栈 +- Vue3(Composition API + `' + result = _save_upload(content, 'a.svg', '.svg') + body = json.loads(result.body) + assert body['ok'] is False + assert 'SVG' in body['msg'] +``` + +### 畸形输入防御测试 + +```python +# tests/unit/utils/test_ip2region.py +from utils.ip2region import _is_private_ip, get_ip_location, get_ip_region + + +def test_is_private_ip_handles_malformed_ip(): + """畸形 IP 不再抛 ValueError,按非内网处理""" + assert _is_private_ip('172.abc') is False + assert _is_private_ip('172.') is False + + +def test_get_ip_location_malformed_ip_degrades_to_unknown(): + """畸形 IP 不抛异常,降级返回"未知" """ + assert get_ip_location('172.abc') == '未知' + + +def test_is_private_ip_private_ranges(): + assert _is_private_ip('172.16.5.5') is True + assert _is_private_ip('172.31.5.5') is True + assert _is_private_ip('172.32.5.5') is False + assert _is_private_ip('10.1.2.3') is True + assert _is_private_ip('192.168.1.1') is True + assert _is_private_ip('8.8.8.8') is False +``` + +### IP 获取测试 + +```python +# tests/unit/utils/test_ip.py +import types +from utils.ip import get_client_ip + + +def _make_request(client_host, xff=None): + """构造模拟 Request 对象。""" + req = types.SimpleNamespace() + req.client = types.SimpleNamespace(host=client_host) + req.headers = {'X-Forwarded-For': xff} if xff else {} + return req + + +def test_xff_single_ip(): + """XFF 只有一个 IP → 直接返回""" + assert get_client_ip(_make_request('10.0.0.1', xff='1.2.3.4')) == '1.2.3.4' + + +def test_xff_multiple_ips_returns_leftmost(): + """XFF 多个 IP → 取最左侧(真实客户端 IP)""" + assert get_client_ip(_make_request('10.0.0.1', xff='1.2.3.4, 10.0.0.1')) == '1.2.3.4' + + +def test_no_xff_falls_back_to_client_host(): + """无 XFF 头 → 回退到 request.client.host""" + assert get_client_ip(_make_request('203.0.113.9')) == '203.0.113.9' + + +def test_no_client_falls_back_to_localhost(): + """client 为 None → 回退到 127.0.0.1""" + req = types.SimpleNamespace(client=None, headers={}) + assert get_client_ip(req) == '127.0.0.1' +``` + +### service 层公共 fixture + +```python +# tests/unit/modules/conftest.py +"""service 层单元测试公共 fixture:构造最小 Request 对象。""" +import types +import pytest + + +@pytest.fixture +def fake_request(): + """模拟 Tornado Request,仅提供 service 层用到的字段。""" + class FR: + state = types.SimpleNamespace(user_id=1, username="tester", realname="测试员") + query_params = {} + return FR() +``` + +## 集成测试 + +验证模块间协作和真实数据库操作(需真实 MySQL/Redis,默认跳过): + +```python +import pytest + +@pytest.mark.integration +class TestPositionIntegration: + """岗位模块集成测试""" + + def test_crud_flow(self, client, auth_headers): + """验证完整的增删改查流程""" + # 添加 + resp = client.post('/api/v1/position/add', json={ + 'name': '集成测试岗位', 'status': 1, 'sort': 1 + }, headers=auth_headers) + assert resp.json()['ok'] is True + + # 查询 + resp = client.get('/api/v1/position/page?pageNo=1&pageSize=10', headers=auth_headers) + assert resp.json()['ok'] is True + assert resp.json()['data']['total'] > 0 +``` + +:::warning 集成测试环境 +集成测试需要真实的 MySQL 和 Redis 实例,默认跳过。运行前确保 `.env` 配置正确,且数据库中已有基础数据。 +::: + +## 运行命令 + +```bash +# 全部测试(跳过集成测试) +pytest +make test + +# 契约测试 +pytest tests/contract -m contract +make test-contract + +# 单元测试 +pytest tests/unit +make test-unit + +# 集成测试(需真实 MySQL/Redis) +pytest tests/integration -m integration +make test-integration + +# 运行指定文件 +pytest tests/unit/modules/test_upload_service.py + +# 运行指定函数 +pytest tests/unit/modules/test_upload_service.py::test_upload_file_rejects_svg + +# 显示详细输出 +pytest -v + +# 显示 print 输出 +pytest -s +``` + +## 测试覆盖率 + +```bash +# 安装覆盖率插件 +pip install pytest-cov + +# 运行测试并生成覆盖率报告 +pytest --cov=src --cov-report=html + +# 查看报告 +open htmlcov/index.html +``` + +:::tip 覆盖率目标 +- Repository 层:80%+ +- Service 层:70%+ +- Endpoint 层:由契约测试保障路由正确性 +::: + +## 总结 + +测试规范采用三层体系:契约快照测试保障路由/权限不变、单元测试验证单函数逻辑、集成测试验证模块协作。测试文件命名 `test_*.py`,函数命名 `test_*`。pytest 标记区分测试类型,默认跳过集成测试。契约快照通过 `UPDATE_GOLDEN=1` 环境变量更新基线。 diff --git a/DjangoAdmin_Tornado_EleVue/zh/why.md b/DjangoAdmin_Tornado_EleVue/zh/why.md new file mode 100644 index 0000000..7f96fda --- /dev/null +++ b/DjangoAdmin_Tornado_EleVue/zh/why.md @@ -0,0 +1,123 @@ +# 为什么选择我们? + +:::tip 核心价值 +企业级数据中台不应从零搭建。本框架提供开箱即用的完整解决方案,为中大型企业构建统一的数字化基座。 +::: + +## 解决什么问题? + +每个企业项目都需要用户管理、权限控制、日志记录、文件上传等基础能力。从零开发这些模块: + +- 耗时 2-4 周,且容易遗漏安全细节 +- 每个项目重复造轮子,维护成本高 +- 缺乏统一规范,代码质量参差不齐 +- 安全防护不全面,上线后才发现漏洞 + +本框架将这些通用能力封装为可复用的模块,新增业务模块只需 **4 个文件 + 1 行注册**: + +``` +models.py → 数据模型定义 +schemas.py → 表单校验规则 +repository.py → 数据访问层 +service.py → 业务逻辑层 ++ 1 行路由注册 → 自动挂载到 API +``` + +## 核心优势 + +### 1. 开箱即用,功能完整 + +预置企业后台所需的全部基础功能,无需额外开发: + +- **系统管理**:用户、角色、菜单、部门、岗位、职级 +- **权限控制**:RBAC 三层模型,按钮级粒度,前后端双重校验 +- **数据管理**:字典、配置、参数,运行时可调 +- **日志审计**:登录日志 + 操作日志,中间件自动记录,无需手动埋点 +- **定时任务**:Cron 调度 + 执行日志,支持动态启停 +- **代码生成器**:根据数据库表一键生成前后端 CRUD 代码,含列表、表单、API 全套 + +### 2. 高度可定制,不侵入核心 + +分层架构 + 模块化设计,业务代码与框架核心完全解耦: + +- **后端三层分离**:Endpoint(HTTP)→ Service(业务)→ Repository(数据),职责清晰 +- **基类复用**:BaseRepository / BaseService 提供 30+ 通用方法,子类只需声明差异 +- **模块可插拔**:业务模块位于 `src/modules/`,按需增删不影响框架 +- **中间件可扩展**:CORS、限流、日志、事务按需注册,链式处理 + +### 3. 多数据库支持,数据可迁移 + +通过 `DB_DRIVER` 环境变量切换数据库,无需修改业务代码: + +| 数据库 | 说明 | +|--------|------| +| MySQL | 默认,生产推荐 | +| PostgreSQL | 企业级首选 | +| SQL Server | 国企/政企场景 | +| SQLite | 开发/测试 | +| Oracle | 金融/传统企业 | + +不仅支持结构迁移,还提供 `scripts/migrate_db.py` 跨库数据泵送工具——从源库到目标库的结构 + 数据整体迁移,含自增序列重置和逐表 COUNT 校验。 + +### 4. 安全体系完善 + +后台系统安全性是刚需,内置多层安全防护: + +- **认证安全**:JWT 令牌 + 黑名单机制,密码 bcrypt 加盐加密 +- **登录防护**:图形验证码防暴力破解、失败自动锁定、密码强度校验 +- **接口防护**:Redis 滑动窗口限流、防重复提交(幂等性) +- **文件上传**:扩展名白名单 + 魔术字节校验 + 路径遍历防护 + 高危格式黑名单 +- **RBAC 权限**:后端装饰器 + 前端指令双重校验,精确到按钮级,管理员自动绕过 +- **数据安全**:软删除机制、敏感字段序列化排除、XSS 清洗 + +### 5. 部署便捷,开箱即用 + +- **Docker 一键启动**:`docker-compose up --build`,含后端 + MySQL + Redis +- **多环境支持**:开发 / 测试 / UAT / 生产,环境变量隔离 +- **进程管理**:Supervisor 配置模板,自动重启、日志轮转 +- **Nginx 配置**:提供企业级反向代理模板,含 HTTPS、Gzip、静态资源缓存 + +### 6. 企业级文档,覆盖全场景 + +97 篇文档覆盖从入门到运维的完整链路: + +``` +快速入门 → 环境搭建、启动调试、第一个接口 +模块开发 → 从 Model 到前端页面的端到端教程 +架构设计 → 分层架构、基类体系、缓存、安全 +开发指南 → 后端核心、业务模块、前端组件、联调 +代码生成器 → CLI / Web UI / 自定义模板 +运维部署 → Docker、Nginx、Supervisor、监控 +规范标准 → API 响应、分页、Git、测试、文档 +``` + +## 适合谁? + +| 场景 | 说明 | +|------|------| +| **快速搭建新项目** | 基础功能开箱即用,专注业务开发 | +| **企业数字化转型** | 稳定可靠的后台框架,多数据库适配 | +| **团队技术统一** | 统一的分层架构和编码规范 | +| **开发者学习** | 完整的企业级项目参考,文档齐全 | + +## 技术栈 + +| 层级 | 技术 | +|------|------| +| 后端 | Tornado + SQLAlchemy + Pydantic | +| 前端 | Vue3 + ElementPlus + TypeScript + Vite | +| 基础设施 | MySQL / PostgreSQL / Redis / Docker | + +详细版本号和技术选型说明见 [技术选型](/zh/volume3/tech-stack)。 + +## 总结 + +作为企业级数据中台解决方案,本框架为中大型企业提供统一的后台管理基座,承载用户、权限、配置、日志等公共能力,让各业务线聚焦于核心业务而非基础设施建设。 + +- **开箱即用**:6 大功能模块预置完备,开箱即用 +- **安全可靠**:6 层安全防护体系,覆盖认证、登录、接口、文件、权限、数据全链路 +- **灵活扩展**:分层架构 + 模块化设计,业务代码与框架核心完全解耦 +- **多库适配**:5 种数据库一键切换,含跨库数据迁移工具 +- **文档齐全**:97 篇企业级文档,从入门到运维全覆盖 + +**核心价值:为中大型企业提供可复用的数字化基座,让每个新项目不再从零搭建基础能力。**