Skip to content

Latest commit

 

History

History
178 lines (137 loc) · 14.6 KB

File metadata and controls

178 lines (137 loc) · 14.6 KB
name fastapiadmin-dev
description FastapiAdmin full-stack dev guide: repo map, run/verify commands, backend module & frontend web conventions. Invoke before writing code, creating modules/pages, debugging, or running builds/tests in this repo.

FastapiAdmin 全栈开发指南

在本仓库做任何开发、调试、构建之前,先按本 skill 对齐工程结构与约定,避免跨端改漏、命名错位。

1. 工程地图

目录 说明 技术栈
backend/ 后端服务(入口 main.py,本地端口见 app/config/setting.py 的 SERVER_PORT,通常 8001) FastAPI + SQLAlchemy + Alembic + Redis + uv
frontend/web/ 管理后台(pnpm 包,Node >= 20) Vue3 + Vite + TS + Element Plus + Tailwind4 + Pinia
frontend/app/ 小程序/移动端 uni-app(有独立约定,见 frontend/app/.agents/skills/wot-ui-*
frontend/docs/ 项目文档站(中英双语 src/guidesrc/en/guide VitePress
docker/ 部署(backend/mysql/nginx/redis) docker-compose
根目录 README.mdREQUIREMENTS.mdCONTRIBUTING.md 需求与规范,做大功能前先读

backend/app 关键位置

位置 说明
modules/<模块>/<功能>/ 业务模块:controller/service/crud/model/schema + 模块根 plugin.toml
plugin/ 独立插件目录(如 plugin/module_example/demo),与 modules 同构、可插拔
api/v1/routers.py 全部路由的注册处
core/ 基建:database、base_crud、base_model、security、permission、dependencies、exceptions、ap_scheduler(定时任务)、sse_manager、middlewares
scripts/initialize.py 启动时自动执行迁移并导入 sql/*.json 种子数据
sql/*.json 菜单/角色/用户/部门/字典/参数等种子数据
templates/{python,ts,vue}/ 代码生成器 jinja2 模板
tests/ pytest(conftest 自建临时库并初始化数据)
utils/ crypto/password/excel/upload/xss 等工具函数

frontend/web/src 关键位置

位置 说明
api/module_x/ API 层,与后端模块一一对应
views/module_x/ 业务页面;页面私有组件放同目录 components/
components/ 公共组件 FaXxx(forms/tables/charts/modal/display 等)——写页面前先找现成的
hooks/core/ 开发套件:useTable、useCrudForm、useCrudDialog、useTableColumns、useAuth、useImportExport、useConfirm
directives/permission/ v-hasPerm 按钮权限指令
router/ routes.ts 静态壳 + guards/MenuProcessor/RouteRegistry 动态路由管道
store/modules/ pinia stores(user/worktab/menu/setting/dict…),持久化用 pinia-plugin-persistedstate
locales/langs/ i18n 词条(zh.json / en.json)
utils/ http(request 封装)、auth、storage、navigation、download、sse 等

2. 常用命令

后端(cd backend,优先用 uv):

uv run main.py run --env=dev          # 启动(dev 默认,DEBUG=True 时自动 reload)
uv run main.py revision -m "说明"      # 生成 Alembic 迁移(autogenerate)
uv run main.py upgrade --env=dev      # 应用迁移到 head
uv run pytest                         # 跑 tests/

前端 web(cd frontend/web):

pnpm i            # 安装
pnpm dev          # 开发
pnpm ts:check     # vue-tsc 类型检查(改完 TS/Vue 必跑)
pnpm lint         # eslint + prettier + stylelint
pnpm test         # vitest
pnpm build        # 构建

frontend/appfrontend/docs 各自目录内 pnpm 管理自己。

首次启动(新人跑通)

  1. 复制 backend/env/.env.example.env.dev,按文件头注释修改必改项(DATABASE_PASSWORD / REDIS_PASSWORD / OPENAI_API_KEY);DATABASE_TYPE 支持 mysql / postgres / sqlite——本地体验用 sqlite 可零配置起跑
  2. uv sync && uv run main.py run --env=dev启动时自动执行迁移并导入 sql/*.json 种子数据(菜单/角色/用户/字典等),无需手动建表导数;dev 环境检测到模型变更还会自动生成并应用迁移
  3. 前端:frontend/webpnpm install && pnpm dev(小程序/App H5 调试用 pnpm dev:h5
  4. 默认账号:super / admin / user,密码均为 123456(已校验种子 bcrypt 哈希;官方文档见 frontend/docs/src/guide/start.md,部署后应立即修改)
  5. 访问地址:Web 前端 http://localhost:{VITE_PORT}frontend/web/.env 为 5180);后端 http://localhost:8001、Swagger http://localhost:8001/docs、API 前缀 /api/v1
  • Docker 一键部署:根目录 ./deploy.sh(详见 docker/README.md

前端代理:vite 将 VITE_APP_BASE_API 前缀代理到 VITE_API_BASE_URL(见 vite.config.ts),本地开发无 CORS 问题;AI WebSocket 用 VITE_APP_WS_ENDPOINT 直连后端(不走代理)。

3. 后端约定(backend/app)

  • 模块结构:app/modules/<模块>/<功能>/,固定文件划分
    • controller.py 路由层、service.py 业务层、crud.py 数据层、model.py ORM、schema.py Pydantic
    • 模块根放 plugin.toml(name/title/version/description)
  • 路由统一在 app/api/v1/routers.py 注册;路由类用 OperationLogRoute(自动操作日志)
  • 权限:Security(AuthPermission(["模块:资源:操作"])),如 module_ai:chat:query
  • 响应:SuccessResponse(data=..., msg=...) + response_model=ResponseSchema[T];业务错误抛 CustomException
  • 基建都在 app/core/base_crud.pybase_model.pybase_schema.pyredis_crud.pydependencies.pyexceptions.pylogger.py(loguru,占位符用 {})
  • 配置:app/config/setting.py + backend/env/.env(模板见 env/.env.example
  • 改了 model 必须生成并执行 Alembic 迁移;初始菜单/角色等数据在 backend/sql/*.json
  • 新模块两种放法:常规业务放 app/modules/,可插拔/示例性质放 app/plugin/(结构相同,都有 plugin.toml
  • 代码生成:backend/templates/{python,ts,vue}/*.jinja2 配合 generator 模块(后台「代码生成」功能可视化生成);生成/修改代码后必须重启后端——dev reload 只在文件变更时重载,动态路由发现(app/core/discover.py)只在启动时执行,不重启新模块不生效且会静默失败

4. 前端 web 约定(frontend/web/src)

  • API 层:api/module_x/xxx.ts,对象字面量方法 + request(来自 @utils),泛型用全局 ApiResponse<T> / PageResult<T>;URL 与后端 controller 对齐(如 /system/dict/type/list
  • 页面:views/module_x/...;页面私有子组件放同目录 components/ 下,公共组件命名 FaXxx
  • CRUD 页面优先复用现成套件:列表用 useTable + FaTable + FaSearchBar,弹窗表单用 useCrudDialog/useCrudForm + FaDialog,列定义用 useTableColumns,导入导出用 useImportExport——参考已有 views/module_system/ 页面写法,不要手写 ElTable/ElDialog
  • 权限控制:按钮级用 v-hasPerm="'sys:user:add'"(支持数组);代码内判断用 useAuth().hasAuth(...);后端标识 模块:资源:操作 三段式,前后端保持一致
  • i18n:文案用 $t('key'),词条加到 locales/langs/zh.jsonen.json 两份
  • 自动导入:vue API(ref/computed/watch/onMounted 等)无需 import;Element Plus 组件模板内直接用;ElMessage 等按现有文件习惯可显式 import
  • 路由:静态壳路由在 router/routes.ts;业务路由来自后端菜单(guards.tsMenuProcessorRouteRegistry 动态 addRoute)。新增页面要在「菜单管理」配置 route_path/route_name/component_path/keep_alive
  • KeepAlive 与工作栏按「组件名」匹配:defineOptions({ name }) 必须与菜单的 route_name 一致,否则页面缓存/缓存排除(exclude)会失灵
  • 有副作用页面(WebSocket/定时器/全局事件监听)必须实现 onActivated/onDeactivated:deactivated 时释放资源,activated 时按需恢复。onUnmounted 只在缓存被驱逐时触发,不能作为唯一清理点(详见第 7 节)
  • 路由视图缓存为单层:目录路由不挂组件,KeepAlive 只存在于 layouts/fa-page-content/index.vue 一处,缓存键是叶子路由 path(query 变化不重挂载,改键逻辑见同文件 routeViewCacheKey
  • 环境变量:公共 .envVITE_APP_BASE_API=/api/v1 请求前缀、VITE_PORT=5180);.env.developmentVITE_API_BASE_URL=http://127.0.0.1:8001 代理目标;AI WebSocket VITE_APP_WS_ENDPOINT=ws://localhost:8001 直连)
  • WebSocket 鉴权(AI chat):token 经 Sec-WebSocket-Protocol["access_token", "access_token." + jwt],后端在握手阶段 websocket_authenticate 校验
  • 图标:FaSvgIcon + iconify(ri: / ep: / line-md:);i18n 用 $t(...)

5. 新增一个业务功能(全栈流程)

  1. 后端建模块(参考 app/modules/system/dict/;标准 CRUD 表可先用后台「代码生成」可视化产出,再调整)
  2. app/api/v1/routers.py 注册路由
  3. uv run main.py revision + upgrade 做迁移;菜单/按钮权限写入 sys_menu(后台「菜单管理」配置 route_path/route_name/component_path/keep_alive/按钮权限)
  4. 前端新建 api/module_x/xxx.tsviews/module_x/ 页面(复用 useTable/FaTable 套件)
  5. 前后端权限标识保持一致(模块:资源:操作),页面按钮加 v-hasPerm
  6. 验证:后端 uv run pytest;前端 pnpm ts:checkpnpm lintpnpm test

6. 提交规范

  • husky + commitlint + git-cz(pnpm commit 交互式生成);type 用 feat/fix/refactor/chore/docs/test 等
  • lint-staged 会自动格式化暂存文件,不要绕过 hook

7. KeepAlive 缓存与连接类资源(踩过坑,勿回退)

  • KeepAlive 的 include 来自 worktab openedlayouts/fa-page-content/index.vue),opened 为空时 include 为 undefined——KeepAlive 对 undefined include 不做 prune,不要以为「标签清了缓存就没了」
  • KeepAlive 内部缓存无法从外部直接清空,唯一手段是改变 include/exclude 触发内部 prune。登出场景由 worktab.store.tsclearAll() 把待删标签组件名写入 keepAliveExclude 驱逐旧实例(下次 openTabremoveKeepAliveExclude 自动移出),改 store 时勿删这段
  • WebSocket 守卫必须覆盖握手期:if (ws && ws.readyState !== WebSocket.CLOSED) return。只挡 OPEN 会在 CONNECTING 期间重入时创建新连接并覆盖旧引用,泄漏的连接照样握手成功并弹提示(AI chat 曾因此登出→登录后出现多条 ws + 多条「连接成功」)
  • 主动断开先摘 onopen/onmessage/onerror/onclose 回调再 close(),避免关闭竞态触发提示或状态回调
  • 路由出口 KeepAlive 只能有一层,且不要给它加 :max:动态目录路由的 component 必须保持 undefinedMenuProcessor.mapMenuNode / RouteTransformer.handleNormalRoute),靠 vue-router 的 RouterView 深度跳级直达叶子页面。历史上给目录挂过壳组件(NestedRouterParent),壳实例会随缓存同时存活多份、同一页面被重复挂载,导致切换菜单时接口重复请求;:max 的 LRU 则会在标签仍打开时挤掉最早的页面,切回时同样无谓重挂载。缓存集合只由 include/exclude 表达
  • 排查「重复弹窗/重复连接/重复请求」类 bug 的路径:先 grep 提示文案定位全库唯一来源(N 次弹窗 = N 个实例或 N 次重入)→ 查 KeepAlive include/exclude 计算与登出→登录导航链(登录守卫 404→replace 重定向会叠加竞态窗口)→ 菜单配置查 backend/sql/sys_menu.json(确认 route_name 唯一、keep_alive)排除后端

8. 已知注意点

  • 静态前端托管:register_frontendapp/__init__.py)检查与挂载必须用同一个 path_conf.FRONTEND_DIST_DIR(backend/dist)。曾因检查用 path_conf、挂载硬编码 frontend/web/dist 导致 500:check_dir=False 时启动不报错,首次请求才炸,必须看 loguru 日志(backend/logs/fastapiadmin.log)才能定位
  • 模板/脚本里不要用 {% for %} + {% set %} 累计布尔标志:Jinja2 for 块作用域隔离,循环外读到的仍是初值。用过滤器一次性计算,如 {% set has_x = columns | selectattr('python_type', 'equalto', 'date') | list | length > 0 %}(代码生成器 schema.py.jinja2 曾因此漏生成 validator import,生成产物 NameError、后端起不来)
  • 跨端需求(web + 小程序)要同时评估 frontend/webfrontend/app 两套代码,API 层各自维护
  • 小程序侧有自己的 skills(frontend/app/.agents/skills/),改小程序 UI 时遵循 wot-ui 约定
  • 文档站改动记得中英两份(src/guide/src/en/guide/

9. 前端产物验证(dist)

源码已修 ≠ 部署已修。 dist 是生成物、不入库(.gitignore),但仓库内存在多份预构建副本并直接被打进部署,改完前端源码必须重建并同步,否则线上仍跑旧行为(AI chat 重复「连接成功」就因此漏判过一轮:源码 e39a369b 已修,三份 dist 仍停在修复前)。

三份产物与各自的服务入口

产物 服务入口
frontend/web/dist 构建输出(pnpm build:prod 的 outDir)
docker/nginx/web/dist docker 部署的 /webnginx.confalias /usr/share/nginx/html/web/dist
backend/dist 一体化部署时 app.frontend("/") 托管(path_conf.FRONTEND_DIST_DIR

移动端同理:docker/nginx/app/dist/build/h5(nginx 的 /app)。

重建 + 同步

cd frontend/web && pnpm build:prod          # 输出 frontend/web/dist
cd ../.. && rsync -a --delete frontend/web/dist/ docker/nginx/web/dist/ \
              && rsync -a --delete frontend/web/dist/ backend/dist/

必须带 --delete:产物文件名带 content hash,不带会把旧 chunk 留在目录里。

验证要核到产物,不能只看 src/(minify 后函数名/注释会丢,但 WebSocket.CLOSED 这类属性访问、以及提示文案字符串会保留):

grep -l "ai/chat/ws" docker/nginx/web/dist/js/*.js                    # 定位 chunk
grep -oE '.{0,60}readyState.{0,60}' <chunk> | grep -i websocket       # 核守卫(-oE 重复上限 255)

例:旧包 if (ws?.readyState === WebSocket.OPEN) return;CLOSED 命中 0 次);修复后 if (ws && ws.readyState !== WebSocket.CLOSED) return;

收到「源码没改好」的线上反馈时,第一步先核 dist 产物特征字符串,再回源码;docker 部署还要重新打镜像/重传 docker 目录才算生效。