--- 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/guide` 与 `src/en/guide`) | VitePress | | `docker/` | 部署(backend/mysql/nginx/redis) | docker-compose | | 根目录 | `README.md`、`REQUIREMENTS.md`、`CONTRIBUTING.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): ```bash 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`): ```bash pnpm i # 安装 pnpm dev # 开发 pnpm ts:check # vue-tsc 类型检查(改完 TS/Vue 必跑) pnpm lint # eslint + prettier + stylelint pnpm test # vitest pnpm build # 构建 ``` `frontend/app`、`frontend/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/web` 内 `pnpm 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.py`、`base_model.py`、`base_schema.py`、`redis_crud.py`、`dependencies.py`、`exceptions.py`、`logger.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` / `PageResult`;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.json` 与 `en.json` 两份 - 自动导入:vue API(`ref`/`computed`/`watch`/`onMounted` 等)无需 import;Element Plus 组件模板内直接用;`ElMessage` 等按现有文件习惯可显式 import - 路由:静态壳路由在 `router/routes.ts`;业务路由来自后端菜单(`guards.ts` → `MenuProcessor` → `RouteRegistry` 动态 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`) - 环境变量:公共 `.env`(`VITE_APP_BASE_API=/api/v1` 请求前缀、`VITE_PORT=5180`);`.env.development`(`VITE_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.ts` 与 `views/module_x/` 页面(复用 useTable/FaTable 套件) 5. 前后端权限标识保持一致(`模块:资源:操作`),页面按钮加 `v-hasPerm` 6. 验证:后端 `uv run pytest`;前端 `pnpm ts:check`、`pnpm lint`、`pnpm test` ## 6. 提交规范 - husky + commitlint + git-cz(`pnpm commit` 交互式生成);type 用 feat/fix/refactor/chore/docs/test 等 - lint-staged 会自动格式化暂存文件,不要绕过 hook ## 7. KeepAlive 缓存与连接类资源(踩过坑,勿回退) - KeepAlive 的 `include` 来自 worktab `opened`(`layouts/fa-page-content/index.vue`),`opened` 为空时 include 为 `undefined`——KeepAlive 对 undefined include **不做 prune**,不要以为「标签清了缓存就没了」 - KeepAlive 内部缓存无法从外部直接清空,唯一手段是改变 `include`/`exclude` 触发内部 prune。登出场景由 `worktab.store.ts` 的 `clearAll()` 把待删标签组件名写入 `keepAliveExclude` 驱逐旧实例(下次 `openTab` 的 `removeKeepAliveExclude` 自动移出),改 store 时勿删这段 - WebSocket 守卫必须覆盖握手期:`if (ws && ws.readyState !== WebSocket.CLOSED) return`。只挡 `OPEN` 会在 CONNECTING 期间重入时创建新连接并覆盖旧引用,泄漏的连接照样握手成功并弹提示(AI chat 曾因此登出→登录后出现多条 ws + 多条「连接成功」) - 主动断开先摘 `onopen/onmessage/onerror/onclose` 回调再 `close()`,避免关闭竞态触发提示或状态回调 - 路由出口 KeepAlive **只能有一层**,且不要给它加 `:max`:动态目录路由的 `component` 必须保持 `undefined`(`MenuProcessor.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_frontend`(`app/__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/web` 与 `frontend/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 部署的 `/web`(`nginx.conf` 的 `alias /usr/share/nginx/html/web/dist`) | | `backend/dist` | 一体化部署时 `app.frontend("/")` 托管(`path_conf.FRONTEND_DIST_DIR`) | 移动端同理:`docker/nginx/app/dist/build/h5`(nginx 的 `/app`)。 **重建 + 同步** ```bash 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` 这类属性访问、以及提示文案字符串会保留): ```bash grep -l "ai/chat/ws" docker/nginx/web/dist/js/*.js # 定位 chunk grep -oE '.{0,60}readyState.{0,60}' | grep -i websocket # 核守卫(-oE 重复上限 255) ``` 例:旧包 `if (ws?.readyState === WebSocket.OPEN) return;`(`CLOSED` 命中 0 次);修复后 `if (ws && ws.readyState !== WebSocket.CLOSED) return;`。 **收到「源码没改好」的线上反馈时,第一步先核 dist 产物特征字符串**,再回源码;docker 部署还要重新打镜像/重传 `docker` 目录才算生效。