init
This commit is contained in:
199
.cursor/rules/admin-framework-routing-store.mdc
Normal file
199
.cursor/rules/admin-framework-routing-store.mdc
Normal file
@@ -0,0 +1,199 @@
|
||||
---
|
||||
description: Admin Framework 动态路由与 Vuex 使用约束(详尽版,对照 admin_core 源码)
|
||||
globs: admin/src/**/*.{vue,js},admin_core/src/**/*.{vue,js}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Admin 路由与状态约束(详尽)
|
||||
|
||||
适用于 `admin` 端动态菜单、路由与 Vuex;**实现以 `admin_core` 源码为准**,模板目录 `admin/src` 与之对齐。涉及文件主要包括:`src/index.js`、`src/router/index.js`、`src/utils/uiTool.js`、`src/utils/http.js`、`src/store/user.js`、`src/store/app.js`、`src/store/index.js`、`src/views/index.js`(`setupComponentMap`)。
|
||||
|
||||
---
|
||||
|
||||
## 1. `createApp` 生命周期与单例
|
||||
|
||||
### 1.1 执行顺序(与 `index.js` 中 `createApp` 一致)
|
||||
|
||||
1. 若未传 `uploadUrl` 且存在 `apiUrl`:按 `apiUrl` 是否以 `/` 结尾拼接 `upload` 或 `/upload`。
|
||||
2. 若 `config.HomePage` 存在:写入框架实例 `this.HomePage`。
|
||||
3. `this.config = config`。
|
||||
4. `Vue.use(ViewUI)`、`Vue.use(VueRouter)`、`Vue.use(Vuex)`。
|
||||
5. 原型注入:`$config`、`$http`、`$tools`、`$uiTool`、`$framework`。
|
||||
6. `registerGlobalComponents(Vue)`:注册布局、表格、上传等全局组件。
|
||||
7. `setupComponentMap(config.componentMap || {}, uiTool)`:合并内置系统页映射与业务映射(见第 3 节)。
|
||||
8. **Store**:若 `!this.store`,则 `createStore(Vuex, {}, null)` 并 `http.init(config, this.store)`;若已有 `store` 则**不重建**、不重复 `http.init`。
|
||||
9. **Router**:若 `!this.router`,则 `getRoutes` + `createRouter`(hash 模式)并挂守卫;若已有 `router` 则**不重建**。
|
||||
10. 根 Vue:`render: h => h('router-view')`,`created` 内:`uiTool.setRem()`、`resize` 监听;若有 `token` + `localStorage.authorityMenus` 则 `dispatch('user/setAuthorityMenus', { Main, ParentView, Page404, HomePage, authorityMenus })` 再 `dispatch('app/getSysTitle', { defaultTitle, defaultLogo })`;否则 `document.title = config.title`;最后调用 `config.onReady`(若存在)。
|
||||
|
||||
### 1.2 单例与全局
|
||||
|
||||
- 默认导出为**同一** `AdminFramework` 实例;`createApp` 前后均可能在浏览器挂载 `window.framework`。
|
||||
- **`store` / `router` 一旦创建即复用**:业务不得在页面里 `new Vuex.Store` 或 `new VueRouter` 替换框架主链路。
|
||||
- `window.framework`、`window.rootVue` **仅供调试**;业务代码不得依赖其存在(SSR、单测、微前端场景可能无 `window` 或未挂载)。
|
||||
|
||||
### 1.3 根实例恢复菜单与标题
|
||||
|
||||
- 刷新后:读 `this.$store.state.user.token` 与 `localStorage.getItem('authorityMenus')`;二者齐全才恢复动态路由并拉标题。
|
||||
- `authorityMenus` 在 `user` 模块里经 `setAuthorityMenus` mutation 写入 **`localStorage.authorityMenus`**(值为 **JSON 字符串**)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 路由模式、基础路由与守卫
|
||||
|
||||
### 2.1 模式与基础表
|
||||
|
||||
- `createRouter` 使用 **`mode: 'hash'`**(`router/index.js`)。
|
||||
- `createBaseRoutes`:`/login`、`/401`、`/404`、`/500`、通配 `*` → 404。
|
||||
|
||||
### 2.2 `setupRouterGuards` 行为摘要
|
||||
|
||||
- 每次导航:`ViewUI.LoadingBar.start()`;`afterEach`:`LoadingBar.finish()` + `window.scrollTo(0,0)`。
|
||||
- **`name === 'view_log'`**:直接 `next()`,不做登录校验(操作日志类独立入口)。
|
||||
- **无 token 且目标非 `login`** → `next({ name: 'login' })`。
|
||||
- **无 token 且目标为 `login`** → `next()`。
|
||||
- **有 token 且目标为 `login`**:若 `from.name === 'home'` 则 `next(false)` 避免重复导航;否则 `next({ name: 'home' })`。
|
||||
- **有 token、`to.matched.length === 0` 且 `to.path !== '/'`**:认为可能动态路由尚未就绪,**150ms** 后 `router.resolve` 再试;仍无匹配则 `next({ name: 'home', replace: true })`。
|
||||
|
||||
### 2.3 动态主路由与 `redirect`
|
||||
|
||||
- 初始 `customRoutes` 来自 `getRoutes`,内部为 `uiTool.getRoutes(Main, ParentView, Page404, HomePage)`,得到 `path: '/'` 的 **主路由** 对象(含 `children`、`redirect`)。
|
||||
- `setAuthorityMenus` 成功生成 `mainMenu` 后:在 `router.options.routes` 中 **删除** 原 `path === '/'` 的项,再 **`router.addRoute(mainMenu)`**;并 `await` 约 **100ms** 等待路由表更新。
|
||||
- **`mainMenu.redirect`**:`getRoutes` 内若存在权限菜单,会 `findFirstRoute`:优先带 `/home` 或 `home` 的项;否则取第一个非「菜单」类型且有 `path` 的节点(菜单类型会递归子节点)。用于登录后首屏落地路径。
|
||||
|
||||
---
|
||||
|
||||
## 3. 菜单数据、类型与组件映射
|
||||
|
||||
### 3.1 菜单项 `type` 与 `menuToRoute`(`uiTool.js`)
|
||||
|
||||
| `type` | 行为 |
|
||||
|--------|------|
|
||||
| `菜单` | `component` 固定为 **`ParentView`**(布局容器) |
|
||||
| `页面` / `功能` | 用 `item.component` 调 **`uiTool.getComponent`**;命中则替换为真实组件;未命中则渲染占位 `Alert`(提示路径并保留 `item.componentPath`);`catch` 时用 **`Page404`** |
|
||||
| 其他 | 视为 `ParentView` |
|
||||
| 任意 | 设置 `meta`:`icon`、`isMenu`、`type`、`title`(来自 `name`);有 `children` 则递归 |
|
||||
|
||||
### 3.2 `getComponent` 与 `setupComponentMap`
|
||||
|
||||
- `getComponent`:去掉路径尾 `.vue` 后与表内键匹配;也支持直接用带 `.vue` 的键。
|
||||
- `setupComponentMap`:先合并默认 `home/index` 与系统页,再展开 **`customMap`**;对每个键生成 **`cleanPath`** 与 **`cleanPath + '.vue'`** 两条映射指向同一组件。
|
||||
- 后端菜单 **`component` 字符串必须与映射键一致**(或仅差 `.vue`,由双键覆盖)。
|
||||
|
||||
### 3.3 首页 `home` 与权限树合并(`getRoutes`)
|
||||
|
||||
- 默认子路由含 **`/home`**(`HomePage` 或内置欢迎 render)。
|
||||
- 若 `localStorage.authorityMenus` 解析后非空:先 `transformTree` 再 `menuToRoute` 得 `curRoutes`。
|
||||
- **`hasHomeInRoutes`**:任意层出现 `path === '/home'` 或 `'home'` 即视为权限自带首页。
|
||||
- **有 home**:`mainRoute.children = curRoutes`(权限树完全替换子级),`redirect` 先置 `/home` 再由 `findFirstRoute` 覆盖。
|
||||
- **无 home**:`mainRoute.children = [homeRoute, ...curRoutes]`,保证仍有框架默认首页。
|
||||
|
||||
---
|
||||
|
||||
## 4. Vuex:`user` 模块(鉴权、菜单、租户)
|
||||
|
||||
### 4.1 `state` 与持久化
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `token` | 初始 `getToken()`(Cookie `token`,见 `tools.js` 中 `TOKEN_KEY`) |
|
||||
| `authorityMenus` | 内存;实际持久化由 mutation 写 **localStorage `authorityMenus`** |
|
||||
| `menuList` | 默认从 **localStorage `menuList`** JSON 解析 |
|
||||
| `currentTenant` | 登录租户;**localStorage `currentTenant`** |
|
||||
| `userName` / `avatorImgPath` | 展示用;`userName` 可与 **localStorage.userName** 同步 |
|
||||
|
||||
### 4.2 `setAuthorityMenus` 入参(action)
|
||||
|
||||
- **`Main`, `ParentView`, `Page404`**:必填语义;用于 `uiTool.getRoutes` / 动态 `addRoute`。
|
||||
- **`HomePage`**:可选;缺省时用 `window.framework.HomePage`(框架不推荐业务依赖 `window`,调用方应显式传入与 `createApp` 一致的 `HomePage`)。
|
||||
- **`authorityMenus`**:可选;若传入则不再请求接口;可为 **已 JSON.stringify 的树** 或对象数组(内部会 `JSON.parse` 字符串)。
|
||||
- **`menuIds`**:可选;仅在 **未传入 `authorityMenus` 且 `authorityMenus()` 接口失败** 时,用于从 **`defaultMenus`**(`menuConfig.js`)过滤。
|
||||
|
||||
### 4.3 `setAuthorityMenus` 数据流摘要
|
||||
|
||||
1. 无 `authorityMenus` → `userServer.authorityMenus()`;`code === 0` 取 `data`;失败则用 `menuIds` + `defaultMenus` 或全量 `defaultMenus`。
|
||||
2. 字符串则 `JSON.parse`;失败或非法数组则回退 `defaultMenus`。
|
||||
3. `commit('setAuthorityMenus', JSON.stringify(menus))`。
|
||||
4. `mainMenu = uiTool.getRoutes(...)`;若有 `children`:`commit('setMenuList', mainMenu.children)`,再按第 2.3 节替换 `/` 路由。
|
||||
|
||||
### 4.4 `handleLogin`(与登录页配合)
|
||||
|
||||
- 期望 **`userServer.login`** 返回体:`{ code, message, data }`,且 `data` 含 **`token`**、**`user.name`**、可选 **`authorityMenus`**(菜单 ID 列表,字符串或数组)、可选 **`tenant`**。
|
||||
- 成功后:`setUserName`、`setToken`、`setCurrentTenant`;解析 `menuIds`;`dispatch('setAuthorityMenus', { Main, ParentView, Page404, HomePage, menuIds })`;再 **`dispatch('app/getSysTitle', {}, { root: true })`**。
|
||||
- 失败:抛错,由页面 `try/catch` 处理。
|
||||
|
||||
### 4.5 `handleLogOut`
|
||||
|
||||
- `setToken('')`、`setAuthorityMenus('[]')`、`setMenuList([])`、`setCurrentTenant(null)`、移除 **`menuList`** 的 localStorage、`location.reload()`。
|
||||
|
||||
---
|
||||
|
||||
## 5. Vuex:`app` 模块(面包屑、站点标题)
|
||||
|
||||
### 5.1 `getSysTitle`(**命名空间 `app`**)
|
||||
|
||||
- 调用:**`this.$store.dispatch('app/getSysTitle', { defaultTitle, defaultLogo })`**;从其他 namespaced 模块内需 **`{ root: true }`**。
|
||||
- 无 `defaultTitle` 时尝试 `window.framework.config.title`,再回退文案。
|
||||
- 有 token:请求 **`paramSetupServer.getOne('sys_title')`**、**`getOne('sys_logo')`**(与参数表约定一致);更新 `document.title` 与 `commit('setSysTitle', formModel)`。
|
||||
- 无 token:仅用默认标题写 `document.title`。
|
||||
|
||||
### 5.2 面包屑
|
||||
|
||||
- `setBreadCrumb` / `setHomeRoute` 由布局与路由变化驱动;业务侧改菜单后应通过 **`setAuthorityMenus`** 刷新路由与 `menuList`,而不是手写面包屑数组。
|
||||
|
||||
---
|
||||
|
||||
## 6. `createStore` 扩展
|
||||
|
||||
```js
|
||||
createStore(Vuex, customModules, createPersistedState)
|
||||
```
|
||||
|
||||
- **模块合并**:`{ user, app, ...customModules }`。
|
||||
- **第三个参数**:传入 **`vuex-persistedstate` 工厂** 则启用 `localStorage` 插件;**`null`**(框架默认)表示不启用持久化插件。
|
||||
|
||||
---
|
||||
|
||||
## 7. HTTP 与全局 Loading(`http.js`)
|
||||
|
||||
### 7.1 实例与 Token
|
||||
|
||||
- **`getHttpInstance`**:`baseURL = config.apiUrl`,合并 `timeout`(默认 300000);若存在 `store.state.user`,设置请求头 **`admin-token`** 为 **`store.state.user.token`**。
|
||||
- Token 存 Cookie(`tools.setToken`),请求头由 HTTP 层统一加;**不要**在业务里手写 `admin-token`。
|
||||
|
||||
### 7.2 响应拦截
|
||||
|
||||
- **HTTP 200** 且 **`response.data.code === 0`**:resolve 整个 `response`(注意:`get`/`post` 的 Promise 里再取 **`response.data`** 作为业务返回值)。
|
||||
- **`code !== 0`**:`hideLoad`、`Message.error(message)`、`reject(message)`(字符串)。
|
||||
- **`401`**:`commit('user/setToken','')`;若存在 **`window.framework.router`** 则 `push({ path: '/login' })`。
|
||||
- 其他网络错误:统一文案映射后 `showError`、`reject`。
|
||||
|
||||
### 7.3 各方法与 Loading
|
||||
|
||||
| 方法 | Loading |
|
||||
|------|---------|
|
||||
| `get` | 默认 `showLoad`;**`config.hideLoad === true`** 时不展示 |
|
||||
| `post` | **始终** `showLoad`(无 `hideLoad` 分支) |
|
||||
| `postFormData` | 无单独 `showLoad`/`hideLoad`(与 `post` 不同,实现上依赖实例 post 行为,以源码为准联调) |
|
||||
| `fileExport` | 独立 `axios.post` + `blob`;默认 `is_down=true` 时调 **`window.framework.uiTool.downloadFile`** |
|
||||
|
||||
### 7.4 参数预处理 `formatParamete`
|
||||
|
||||
- 深拷贝后对**对象**递归删除值为 **`''`** 的键;`Date` 格式化为 **`YYYY-MM-DD HH:mm:ss`**。
|
||||
|
||||
---
|
||||
|
||||
## 8. 组件复用优先级(与框架导出一致)
|
||||
|
||||
- 表单弹窗:**`editModal`**、**`AsyncModal`**;字段:**`FieldRenderer`**、**`fieldItem`**。
|
||||
- 表格/树/上传:**`Tables`**、**`TreeGrid`**、**`UploadSingle`** / **`UploadMultiple`**。
|
||||
- 布局:**`Main`**、**`ParentView`**;勿复制一套侧边栏 + 多页签逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 9. 联调检查清单(路由 / Store)
|
||||
|
||||
- [ ] 菜单里「页面/功能」的 `component` 路径是否能在映射表命中?
|
||||
- [ ] 改菜单后是否调用 **`user/setAuthorityMenus`**(并传入布局组件),而非只改 DB?
|
||||
- [ ] 刷新后标题是否仍走 **`app/getSysTitle`**?
|
||||
- [ ] 新增无感轮询是否用 **`get` + `hideLoad`**,而非 `post`?
|
||||
- [ ] 是否避免在业务逻辑里依赖 **`window.framework`** 完成核心流程?
|
||||
114
.cursor/rules/admin-framework-strict.mdc
Normal file
114
.cursor/rules/admin-framework-strict.mdc
Normal file
@@ -0,0 +1,114 @@
|
||||
---
|
||||
description: Admin 前端 admin-framework 严格约束(详尽版,对照 admin_core)
|
||||
globs: admin/src/**/*.{vue,js},admin_core/src/**/*.{vue,js}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Admin Framework 严格约束(详尽)
|
||||
|
||||
本规则用于 `admin` / `admin_core` 业务页面与入口,**优先级高于建议类文档**。与 `admin_core` 中 `http.js`、`store/user.js`、`router/index.js`、`index.js` 行为冲突的写法一律禁止。
|
||||
|
||||
---
|
||||
|
||||
## 1. 禁止项(违反即应重构)
|
||||
|
||||
### 1.1 HTTP 与 axios
|
||||
|
||||
- **禁止**在业务 `vue/js` 中 `import axios` 或 `axios.create`,用于替代 `this.$http`(`http.js` 已集中处理拦截器、loading、`admin-token`)。
|
||||
- **禁止**手工设置请求头 **`admin-token`**(或复制一套与 `getHttpInstance` 等价的拦截器);例外仅存在于框架内部的 **`fileExport`**(独立 `axios.post`),业务仍应调用 **`this.$http.fileExport`**。
|
||||
- **禁止**在页面内维护第二套 **`apiUrl` / baseURL** 常量拼接 URL;统一 `createApp({ apiUrl })` 与 **`this.$http`**。
|
||||
|
||||
### 1.2 与框架重复的基础设施
|
||||
|
||||
- **禁止**复制 **`formatParamete`**(去空串、日期格式化)、全局 Loading DOM(`spin-box-one`)、401 跳转登录等与 **`http`** 重复的逻辑。
|
||||
- **禁止**在已有 **`editModal` / `AsyncModal` / `FieldRenderer`** 能满足需求时,再写一套通用弹窗/动态表单渲染(除非产品明确要求完全不同的交互且与框架无关)。
|
||||
|
||||
### 1.3 路由与菜单
|
||||
|
||||
- **禁止**直接写 **`localStorage.authorityMenus`**、**`localStorage.menuList`** 替代 **`user` 模块 mutation/action**(除非你在做迁移脚本且不在运行时页面)。
|
||||
- **禁止**绕过 **`setAuthorityMenus`** 直接 **`router.addRoutes`** 拼装主布局(会与框架内 **`router.options.routes` splice + addRoute** 逻辑冲突)。
|
||||
|
||||
### 1.4 命名与兼容
|
||||
|
||||
- **禁止**接口/表单字段 **`snake_case` 与 `camelCase` 双读兜底**(如 `row.user_id ?? row.userId`),除非后端合同明文双写且规范文档特许。
|
||||
- **禁止**对框架已约定 API 的 **`ref`** 写 **`typeof ref.show === 'function'`**、**`if (!this.$refs.modal) return`** 等防御式分支(规范用法下 ref 由模板绑定保证;若条件渲染导致 ref 不稳定,应调整 `v-if` 与调用时机,而非堆兼容分支)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 必须项(代码审查硬标准)
|
||||
|
||||
### 2.1 请求与错误
|
||||
|
||||
- **必须**使用 **`this.$http`**(组件)或 **`AdminFramework.http`**(非组件);二者指向同一封装实例。
|
||||
- **必须**对 `await this.$http.*` 使用 **`try/catch`**(或等价的 `.catch`);原因:后端 **`code !== 0`**、网络错误、401 均可能 **reject**;拦截器已 `Message.error` 时,catch 内可避免重复弹窗,但**不可**空 catch 吞掉需上报的逻辑。
|
||||
- **必须**对**写库、改状态**类接口优先使用 **`POST`**,路径 **`snake_case`** 分段(与后端控制器风格一致)。
|
||||
|
||||
### 2.2 Loading 策略
|
||||
|
||||
- **必须**用 **`this.$http.get(url, params, { hideLoad: true })`** 实现无感轮询/预加载;**不得**为绕过 `post` 的强制 loading 而引入 axios。
|
||||
- 若业务确需「POST 且无 loading」,**正确做法**是扩展 **`admin_core` 的 `http.post`**(例如增加可选 `config.hideLoad`)并升版框架,**禁止**仅在业务仓库私改 axios。
|
||||
|
||||
### 2.3 动态路由与 `componentMap`
|
||||
|
||||
- **必须**保证后端菜单「页面/功能」行的 **`component`** 在 **`setupComponentMap` / `addComponentMap`** 合并后的表可解析。
|
||||
- **必须**在新菜单页合并前本地验证:**`uiTool.getComponent(菜单路径)`**(或通过实际登录 + 点击菜单)无「组件未找到」占位。
|
||||
- 映射键:**建议**与菜单字符串完全一致;框架虽自动补 **无 `.vue` / 有 `.vue`** 双键,但**路径前缀、大小写、子目录**仍须一致。
|
||||
|
||||
### 2.4 刷新菜单与标题
|
||||
|
||||
- 菜单 CRUD 后刷新侧栏:**必须** `dispatch('user/setAuthorityMenus', { Main, ParentView, Page404, HomePage })`(参数与 **`sys_menu.vue`** 等处一致,使用 **`this.$framework.components`** 与 **`this.$framework.pages`** / **`HomePage`**)。
|
||||
- 修改站点标题/Logo 后:**必须** `dispatch('app/getSysTitle', { defaultTitle: this.$config.title, defaultLogo: '' })`(**带 `app/` 命名空间**)。
|
||||
|
||||
### 2.5 框架组件 `ref` 调用
|
||||
|
||||
- **`AsyncModal`、`FloatPanel`、`editModal`**:**直接** `this.$refs.xxx.show()` / `hide()` / 文档约定方法;**禁止**多套 hide 回退、反射内部 state。
|
||||
- 若组件为 `v-if` 控制,**必须**在 **`$nextTick`** 后再调 `ref` 方法,而不是写「若不存在则静默 return」掩盖时序 bug。
|
||||
|
||||
### 2.6 子组件内聚
|
||||
|
||||
- 表单 **`data` / `rules` / `loading` / `visible`**、校验、重置:**必须**放在子组件内;父组件只传 **`id`**、**只读上下文**、**`@success`** 等;**禁止**父组件 `v-model` 绑十几项内部字段。
|
||||
|
||||
---
|
||||
|
||||
## 3. `createApp` 入口(硬约束)
|
||||
|
||||
- **必须** `AdminFramework.createApp(config)` 得到根实例后再 **`$mount`**。
|
||||
- **`config.apiUrl` 必填**;`title`、`componentMap`、`HomePage`、`onReady` 按项目需要。
|
||||
- **不得**在业务页重复 `Vue.use(VueRouter)` / `Vuex` / `ViewUI` 以替换框架已注册插件(二次 `createApp` 也不会卸载已 `Vue.use` 的插件,易脏状态)。
|
||||
|
||||
### 3.1 运行时补映射
|
||||
|
||||
- **必须**使用 **`AdminFramework.addComponentMap(map)`**;**禁止**直接改 **`uiTool`** 内部 `componentMap` 对象引用。
|
||||
|
||||
---
|
||||
|
||||
## 4. Token 与 Cookie(与 `tools.js` 一致)
|
||||
|
||||
- Cookie 键 **`TOKEN_KEY === 'token'`**(不是 `admin-token`);**HTTP 请求头**才是 **`admin-token`**,值来自 **`store.state.user.token`**。
|
||||
- 登录/登出须走 **`user`** 模块的 **`setToken` / `handleLogOut`**,保证 Cookie 与 store 一致。
|
||||
|
||||
---
|
||||
|
||||
## 5. 反例与正例(摘要)
|
||||
|
||||
| 反例 | 正例 |
|
||||
|------|------|
|
||||
| `import axios from 'axios'; axios.get(...)` | `await this.$http.get(...)` |
|
||||
| `headers: { 'admin-token': xxx }` | 使用 `this.$http`,token 由 store 注入 |
|
||||
| `dispatch('getSysTitle')` | `dispatch('app/getSysTitle', { ... })` |
|
||||
| `post` 轮询且无 loading | `get` + `{ hideLoad: true }` 或扩展框架 |
|
||||
| `receiving_id ?? receivingId` | 只使用合同字段名(如 `receiving_id`) |
|
||||
| `if (this.$refs.m && this.$refs.m.show) this.$refs.m.show()` | `this.$nextTick(() => this.$refs.m.show())` |
|
||||
|
||||
---
|
||||
|
||||
## 6. 提交前自检(详尽清单)
|
||||
|
||||
- [ ] 全文搜索 **`import axios`** / **`axios.create`**(业务目录应为 0)。
|
||||
- [ ] 全文搜索 **`admin-token`**(除注释外应为 0,框架内 `fileExport` 除外若复制代码)。
|
||||
- [ ] 变更接口是否 **`POST`** + **`snake_case`** URL。
|
||||
- [ ] 新页面是否已写入 **`componentMap`**(或运行时 **`addComponentMap`**)。
|
||||
- [ ] 标题刷新是否 **`app/getSysTitle`**。
|
||||
- [ ] 所有 **`await this.$http`** 是否处于 **`try/catch`** 或链式 **`catch`**。
|
||||
- [ ] 是否存在 **`??` 双字段名**、**`typeof === 'function'` ref 防御**(应删除或改为规范时序)。
|
||||
- [ ] 子组件是否仍接收大量「仅表单内部使用」的 props(应内聚)。
|
||||
290
.cursor/rules/admin-framework-usage.mdc
Normal file
290
.cursor/rules/admin-framework-usage.mdc
Normal file
@@ -0,0 +1,290 @@
|
||||
---
|
||||
description: Admin 前端 admin-framework 使用约定(详尽版,对照 admin_core 与 README)
|
||||
globs: admin/src/**/*.{vue,js},admin_core/src/**/*.{vue,js}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Admin Framework 使用约定(详尽)
|
||||
|
||||
适用于 **Vue 2 + Vue Router 3 + Vuex 3 + View Design** 的后台;约定与 **`admin_core` 源码**、根目录 **`README.md`**、分发 **`admin-framework.md`** 一致。默认构建产物为 **`dist/admin-framework.js`(UMD)**,浏览器全局为 **`window.AdminFramework`** / **`window.framework`**(单例)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 依赖与引入
|
||||
|
||||
### 1.1 Peer 依赖(须由宿主安装)
|
||||
|
||||
`vue`、`vue-router`、`vuex`、`view-design`、`axios` 等须满足 **`package.json` 中 peerDependencies**;打包时将框架作 externals 或正常解析均可,须与 UMD externals 一致。
|
||||
|
||||
### 1.2 入口最小示例
|
||||
|
||||
```javascript
|
||||
import AdminFramework from 'admin-framework' // 或 dist 路径
|
||||
import componentMap from './router/component-map.js'
|
||||
|
||||
const app = AdminFramework.createApp({
|
||||
title: '某某管理系统',
|
||||
apiUrl: 'https://api.example.com/admin_api/',
|
||||
componentMap,
|
||||
HomePage: null, // 可选:自定义首页组件
|
||||
onReady() {
|
||||
// this 为根 Vue 实例
|
||||
}
|
||||
})
|
||||
app.$mount('#app')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. `createApp(config)` 配置项
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `apiUrl` | string | **必填**;`http` 的 `baseURL` |
|
||||
| `uploadUrl` | string | 可选;不传则按 `apiUrl` 自动拼 `upload` 或 `/upload` |
|
||||
| `title` | string | 默认 `document.title`、未登录或 `getSysTitle` 失败时的标题基线 |
|
||||
| `componentMap` | object | 业务页组件路径 → 组件构造器;与菜单 `component` 字段对应 |
|
||||
| `HomePage` | Component | 可选;覆盖默认欢迎首页 |
|
||||
| `onReady` | function | 可选;根实例 `created` 末尾、`this` 指向根实例 |
|
||||
|
||||
**执行后**可用:`AdminFramework.store`、`AdminFramework.router`、`AdminFramework.config`、`AdminFramework.http`、`AdminFramework.uiTool`、`AdminFramework.tools` 等(与单例上字段一致)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 框架单例 API 速查(`index.js` / README)
|
||||
|
||||
| API | 用途 |
|
||||
|-----|------|
|
||||
| `createApp(config)` | 一键创建 store、router、根实例 |
|
||||
| `addComponentMap(customMap)` | 运行时合并组件映射(内部 `setupComponentMap`) |
|
||||
| `initHttp(config, store)` | 非 `createApp` 场景手动绑定 http 与 store |
|
||||
| `version` | 版本字符串 |
|
||||
| `pages` | 内置登录、错误页、部分系统页导出 |
|
||||
| `components` | **Main**、**ParentView**、**Tables**、**AsyncModal**、**editModal**、**FieldRenderer** 等 |
|
||||
| `systemApi` | `src/api/system` 聚合(框架自带系统接口封装) |
|
||||
| `storeModules` | 默认 `user`/`app` 模块引用,扩展 store 时可参考 |
|
||||
| `createBaseRoutes` / `setupRouterGuards` / `createRouter` / `getRoutes` | 高级自定义路由时使用 |
|
||||
| `registerComponents` / `registerGlobalComponents` | 高级注册组件 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 全局注入(`createApp` 内)
|
||||
|
||||
- **`Vue.prototype.$config`**:`createApp` 的 config。
|
||||
- **`$http`**、`$tools`**、`$uiTool`**、`$framework`**:与单例一致。
|
||||
|
||||
组件内推荐 **`this.$http`** / **`this.$uiTool`**;单元测试或非组件模块可用 **`AdminFramework.http`**。
|
||||
|
||||
---
|
||||
|
||||
## 5. HTTP 模块(`src/utils/http.js`)
|
||||
|
||||
### 5.1 成功约定
|
||||
|
||||
- 后端 JSON:**`code === 0`** 为业务成功;否则拦截器 **`Message.error`** 并 **reject**。
|
||||
- `get`/`post` 返回的 Promise **resolve 值为 `response.data`**(即整包 `{ code, message, data }`),业务通常再判断 **`res.code === 0`** 后使用 **`res.data`**。
|
||||
|
||||
### 5.2 方法一览
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `get(url, param, config)` | Query;**`config.hideLoad`** 为 true 时不显示全局 loading |
|
||||
| `post(url, param, config)` | JSON body;**始终**显示 loading |
|
||||
| `postFormData(url, data)` | `application/x-www-form-urlencoded` |
|
||||
| `fileExport(url, param, filename, is_down)` | `blob` 下载;默认 **`is_down=true`** 时内部 **`uiTool.downloadFile`** |
|
||||
| `baseUrl()` | 当前 `apiUrl` |
|
||||
| `ImgSrc(src)` | `baseUrl() + src` |
|
||||
|
||||
### 5.3 401 行为
|
||||
|
||||
- 清空 **`user.token`**;依赖 **`window.framework.router`** 跳转 **`/login`**(布局代码应避免在非浏览器环境假设 router 已挂 window)。
|
||||
|
||||
---
|
||||
|
||||
## 6. uiTool 模块(README §6 整段,`src/utils/uiTool.js`)
|
||||
|
||||
通过 **`this.$uiTool`** 或 **`AdminFramework.uiTool`** 使用(**类静态方法**)。
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `setComponentMap(map)` | 合并组件映射表 |
|
||||
| `getComponent(path)` | 按路径取组件,`path` 可带或不带 `.vue` |
|
||||
| `downloadFile(res, fileName)` | Blob 下载 |
|
||||
| `setRem()` | 根据屏宽设置根字体(自适应) |
|
||||
| `getImgSrc(src)` | 图片地址(同 http 规则) |
|
||||
| `getBtn(h, options)` | Render 函数里生成操作按钮组 |
|
||||
| `getDropdown(h, items)` | 下拉更多菜单 |
|
||||
| `delConfirm(callback)` | 删除确认框 |
|
||||
| `showConfirm({ title, content }, callback)` | 通用确认框 |
|
||||
| `transformTree(list, cb)` | 扁平列表转树 |
|
||||
| `subTree` / `menuToRoute` / `getRoutes` | 菜单转路由(框架内部与权限菜单配合) |
|
||||
|
||||
**树表示例:**
|
||||
|
||||
```javascript
|
||||
const tree = this.$uiTool.transformTree(flatListFromApi)
|
||||
```
|
||||
|
||||
**与路由、菜单的衔接**:`menuToRoute`、`getRoutes` 与动态权限、`componentMap` 的配合见 **`admin-framework-routing-store.mdc`**;`downloadFile` 与 **`this.$http.fileExport`** 配合见本文 §5。
|
||||
|
||||
---
|
||||
|
||||
## 6.2 通用工具 `tools`(README §6.2,`src/utils/tools.js`)
|
||||
|
||||
通过 **`this.$tools`** 使用(具体以源码为准)。
|
||||
|
||||
常用包括:
|
||||
|
||||
- **Cookie / Token**:`getToken`、`setToken`、`TOKEN_KEY`
|
||||
- **日期**:`formatDate(val, fmt)`(dayjs)
|
||||
- **数组**:`forEach`、`hasOneOf`、`getUnion`、`getIntersection`
|
||||
- **对象**:`objEqual`、`removeEmptyObject`、`isNullorEmpty`
|
||||
- **路由/菜单**:`getBreadCrumbList`、`getHomeRoute`、`getMenuByRouter`、`filterMenu`
|
||||
- **本地存储**:`localSave`、`localRead`
|
||||
- **其它**:`generateUUID`、`getUrlParam`、`downStream`、`scrollTop` 等
|
||||
|
||||
**示例:**
|
||||
|
||||
```javascript
|
||||
if (this.$tools.getToken()) { /* 已登录 */ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6.3 表格组件 `Tables`(README §12.2)
|
||||
|
||||
基于 View Design **`Table`** 封装,支持分页条、导出、列配置、可编辑列等。
|
||||
|
||||
| Props | 说明 |
|
||||
|-------|------|
|
||||
| `value` | 表格数据数组 |
|
||||
| `columns` | 列配置(同 iView Table `columns`,框架内会做对齐等默认处理) |
|
||||
| `title` / `tip` | 表格标题与灰色提示 |
|
||||
| `isDown` | 为 true 且 `value` 非空时显示「下载」链接触发导出 |
|
||||
| `pageOption` | 分页:`{ page, pageSize, total }`,存在则显示 `Page` |
|
||||
| `width` / `height` | 传给内部 Table |
|
||||
| `maxHeightOffset` | 表格外层最大高度:`auto`(默认,按视口计算)或数字,用于 `calc(100vh - offset)` |
|
||||
|
||||
| 插槽 | 说明 |
|
||||
|------|------|
|
||||
| `header` | 标题行右侧区域 |
|
||||
| 默认 | 透传给内部 Table(如 `slot-scope` 列) |
|
||||
| `footer` / `loading` | 透传 Table 同名插槽 |
|
||||
|
||||
| 事件 | 说明 |
|
||||
|------|------|
|
||||
| `changePage` | 分页变更(内部已更新 `pageOption.page`) |
|
||||
| `on-select` | 多选变化 |
|
||||
| `downExecl` | 点击下载时,`{ value, columns }` |
|
||||
|
||||
```vue
|
||||
<Tables
|
||||
:value="tableData"
|
||||
:columns="columns"
|
||||
:page-option="{ page: 1, pageSize: 10, total: 100 }"
|
||||
title="用户列表"
|
||||
@changePage="loadData"
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 系统 API(`systemApi`,README §8 整段)
|
||||
|
||||
`AdminFramework.systemApi` 聚合导出 **`admin_core/src/api/system/index.js`** 中的服务(与源码 `export { default as ... }` 一致;以下为当前版本完整导出)。
|
||||
|
||||
| 导出名 | 模块文件 | 典型用途 |
|
||||
|--------|----------|----------|
|
||||
| `fileServe` | `fileServe.js` | 文件上传下载 |
|
||||
| `plaAccountServer` | `pla_account_server.js` | 平台账号管理 |
|
||||
| `rolePermissionServer` | `rolePermissionServer.js` | 角色权限 |
|
||||
| `roleServer` | `roleServer.js` | 角色管理 |
|
||||
| `sysAddressServer` | `sysAddressServer.js` | 地址字典 |
|
||||
| `sysModuleServer` | `sysModuleServer.js` | 模块管理 |
|
||||
| `sysLogServe` | `sys_log_serve.js` | 系统日志 |
|
||||
| `systemTypeServer` | `systemType_server.js` | 系统类型配置 |
|
||||
| `tableServer` | `tableServer.js` | 表结构相关 |
|
||||
| `userServer` | `userServer.js` | 登录、用户 CRUD、权限菜单 |
|
||||
| `formFieldServer` | `formFieldServer.js` | 表单字段 |
|
||||
| `formServer` | `formServer.js` | 表单模型 |
|
||||
| `menuServer` | `menuServer.js` | 菜单管理 |
|
||||
| `modelFieldServer` | `modelFieldServer.js` | 数据模型字段 |
|
||||
| `modelServer` | `modelServer.js` | 数据模型 |
|
||||
| `paramSetupServer` | `paramSetupServer.js` | 系统参数(标题、Logo) |
|
||||
| `sysControlTypeServer` | `sysControlTypeServer.js` | 控件类型 |
|
||||
| `sysTenantServer` | `sysTenantServer.js` | 租户管理 |
|
||||
|
||||
**调用示例:**
|
||||
|
||||
```javascript
|
||||
const { userServer } = AdminFramework.systemApi
|
||||
|
||||
async function login() {
|
||||
const res = await userServer.login({ username: 'admin', password: '***' })
|
||||
if (res.code === 0) {
|
||||
// 写入 token、拉菜单等由登录页与 store 配合完成
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
实际请求仍走全局配置的 **`http`**(`apiUrl`、`admin-token` 头)。
|
||||
|
||||
> **说明**:`src/api/system` 中若存在**未**在 **`index.js`** 聚合导出的文件(例如实验性模块),**不会**出现在 **`AdminFramework.systemApi`**;新增系统接口时须同时 **`export`** 才能在框架聚合对象上访问。
|
||||
|
||||
---
|
||||
|
||||
## 8. Vuex 内置模块
|
||||
|
||||
### 8.1 `user`(namespaced)
|
||||
|
||||
- **state**:`token`、`userName`、`authorityMenus`、`menuList`、`currentTenant`、`avatorImgPath` 等。
|
||||
- **常用 action**:**`handleLogin`**、**`handleLogOut`**、**`setAuthorityMenus`**。
|
||||
- **租户**:**`currentTenant`** 与 **`localStorage.currentTenant`** 由 **`setCurrentTenant`** 同步。
|
||||
|
||||
### 8.2 `app`(namespaced)
|
||||
|
||||
- **state**:**`sysFormModel`**(标题、Logo)、**`breadCrumbList`**、**`homeRoute`**。
|
||||
- **action**:**`getSysTitle`** — 必须使用 **`app/getSysTitle`** 形式 dispatch。
|
||||
|
||||
### 8.3 扩展 Store
|
||||
|
||||
```javascript
|
||||
import { createStore } from 'admin-framework/src/store' // 按实际包路径调整
|
||||
import createPersistedState from 'vuex-persistedstate'
|
||||
|
||||
// 第三个参数传工厂函数则启用持久化
|
||||
const store = createStore(Vuex, { myModule }, createPersistedState)
|
||||
```
|
||||
|
||||
默认 **`createApp`** 内为 **`createStore(Vuex, {}, null)`**,即**无** persistedstate 插件。
|
||||
|
||||
---
|
||||
|
||||
## 9. 路由
|
||||
|
||||
- **Hash 模式**(`router/index.js`)。
|
||||
- **动态菜单**:依赖 **`localStorage.authorityMenus`** + **`setAuthorityMenus`** 与 **`router.addRoute`**;详见 routing-store 文档。
|
||||
- **守卫**:未登录跳转登录、已登录访问登录重定向 **`home`**、动态路由未命中延迟重试等,见 routing-store 文档。
|
||||
|
||||
---
|
||||
|
||||
## 10. 与后端协作约定
|
||||
|
||||
- **管理端 API** 路径通常带 **`/admin_api/`** 前缀(以项目 `apiUrl` 为准)。
|
||||
- **请求体/查询**:与 Node 端 **`snake_case`** 字段对齐;前端展示层若用驼峰,应在边界单次映射,避免全页双写。
|
||||
- **分页**:若后端使用框架 **`ctx.getPageSize`**,则与 **`pageOption`** JSON 约定一致(见 node-core 规范)。
|
||||
|
||||
---
|
||||
|
||||
## 11. 调试与排错
|
||||
|
||||
- **`window.framework`**:单例、router、store、config。
|
||||
- **`window.rootVue`**:`createApp` 返回的根实例。
|
||||
- 动态页空白:控制台搜 **「组件未找到」**;检查菜单 **`component`** 与 **`componentMap`**。
|
||||
- 刷新后丢菜单:检查 **`token`** 与 **`localStorage.authorityMenus`** 是否仍存在、是否执行了 **`setAuthorityMenus`**。
|
||||
|
||||
---
|
||||
|
||||
## 12. 版本与构建
|
||||
|
||||
- 框架版本见 **`AdminFramework.version`** 与 **`README`**;发版时注意 **`admin-framework.js`** 与 **`admin-framework.md`** 同步分发。
|
||||
262
.cursor/rules/node-core-framework-strict.mdc
Normal file
262
.cursor/rules/node-core-framework-strict.mdc
Normal file
@@ -0,0 +1,262 @@
|
||||
---
|
||||
description: Node Core(Koa2+Sequelize)启动、路由、鉴权与 Context 严格约束(详尽版,对照 node_core)
|
||||
globs: "{app.js,api/**/*.js,config/**/*.js,middleware/**/*.js,services/**/*.js,models/**/*.js,node_core/**/*.js}"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Node Core 严格约束(详尽)
|
||||
|
||||
适用于基于 **`node_core`** 的业务后端;行为以 **`node_core/framework.js`**、**`middleware/baseRequest.js`**、**`router/baseController.js`** 及分发 **`node-core-framework.md`** 为准。
|
||||
|
||||
---
|
||||
|
||||
## 1. 静态入口 `Framework.init(config)` 执行顺序
|
||||
|
||||
模块导出 **`init`**(`framework.js` 末尾),顺序**固定**:
|
||||
|
||||
1. **`new Framework(config)`**
|
||||
- 构造内 **`_validateConfig(config)`**:缺 `db` 必填子项、`apiPaths` 非法等会 **抛错终止**;`baseUrl` 缺失、`allowUrls` 非数组、`redis` 不完整等多为 **警告**。
|
||||
- 创建 **`ServiceManager`**、**`RequestManager`**;挂载 **`initDb`**、**`initApi`** 方法引用。
|
||||
|
||||
2. **`_validatePackages()`**
|
||||
- 若 **`config.skipPackageValidation === true`** 则跳过。
|
||||
- 否则 **`PackageValidator.validate`**;若 **`strictPackageValidation === true`** 且失败则 **抛错**;否则仅 **warn**。
|
||||
|
||||
3. **`await initDb(config.modelPaths, config.businessAssociations)`** → 内部 **`_initDb`**:
|
||||
- **`await _validateLicense(licensePath)`**:无授权码会 **`process.exit(1)`**(硬退出)。
|
||||
- **`ModelManager`** 初始化库、**`initSysModels`**、**`_loadBusinessModels`**(目录下 `*.js`,函数则 `(db) => model(db)`)、**`mergeModels`**、**`setupBusinessModelAssociations`**(若传入)。
|
||||
- 设置 **`dbInitialized = true`**。
|
||||
|
||||
4. **`await initServices()`**
|
||||
- **`serviceManager.initServices()`**(含 Redis 等异步);默认 **`servicesRequired !== false`** 时失败 **抛错**;若为 **`false`** 则 warn 并继续。
|
||||
|
||||
5. **`await initApi(config.beforeInitApi)`** → 内部 **`_initApi`**:
|
||||
- 若 **`apiInitialized`** 已 true:warn 并 return。
|
||||
- 若 **`!dbInitialized`**:**抛错** `Database must be initialized before API`。
|
||||
- 然后进入 **§2** 管线。
|
||||
|
||||
6. **`return global_framework`**。
|
||||
|
||||
**业务侧**:`const framework = await Framework.init({ ... }); await framework.start(port);`;不得在 **`init` resolve 之前** 假设 DB/API 已就绪。
|
||||
|
||||
---
|
||||
|
||||
## 2. `_initApi` 内部顺序(中间件与路由)
|
||||
|
||||
1. **`_setupMiddleware()`**(顺序即 Koa 洋葱外层 → 内层)
|
||||
- **`koa-static`**:`path.resolve(__dirname, "../upload")`(相对 **`node_core` 包内位置**),`extensions: ["html"]`。
|
||||
- **`@koa/cors`**:`exposeHeaders: ["*"]`。
|
||||
- **`koa-body`**:`multipart: true`,**`maxFileSize: 200 * 1024 * 1024`**(200MB)。
|
||||
- **`this.requestManager.getBaseRequest()`**:Token 校验、**`ctx.*` 扩展**、日志等(见 §4)。
|
||||
|
||||
2. **`await beforeInitApi(this)`**(若存在)
|
||||
- **仅此阶段**应调用 **`framework.addRoutes`**、**`framework.use`** 等,保证在 **Swagger 与路由表注册之前** 插入中间件或收集自定义路由。
|
||||
|
||||
3. **`_setupSwagger()`**(当构造时存在 `apiPaths` 即会调用;与 `_initApi` 内注释一致)
|
||||
- 使用 **`modelManager.getAllModels()`** 等创建 Swagger 服务并挂载文档路由。
|
||||
|
||||
4. **`_setupRoutes(apiPaths)`**
|
||||
- **`_registerSystemControllers`**:固定加载 **`controller/admin`** 下 **sys_*** 模块,前缀 **`/admin_api`**(见 §3)。
|
||||
- **`baseController.loadControllers(apiPaths)`**:扫描业务目录。
|
||||
- 将 **`this.customRoutes`**(由 **`addRoutes`** 累积)**push** 到 **`baseController.routes`**。
|
||||
- **`baseController.init(router)`** 注册到 **koa-router**,再 **`app.use(router.routes())`**、**`router.allowedMethods()`**。
|
||||
|
||||
5. **`apiInitialized = true`**。
|
||||
|
||||
---
|
||||
|
||||
## 3. 系统控制器与业务控制器
|
||||
|
||||
### 3.1 系统模块(`_registerSystemControllers`)
|
||||
|
||||
手动 `require` 并注册(避免 webpack 动态路径问题):
|
||||
|
||||
- **`sys_user`**、**`sys_menu`**、**`sys_role`**、**`sys_tenant`**、**`sys_parameter`**、**`sys_log`**、**`sys_control_type`**
|
||||
|
||||
路由前缀:**`/admin_api`** + 各导出键路径。
|
||||
|
||||
### 3.2 业务 `apiPaths`
|
||||
|
||||
- 元素为 **字符串**:仅 **`path`**(目录),**无前缀**。
|
||||
- 元素为 **对象**:**`path`**(必填),**`prefix`**(可选),**`authType`**(可选,见 §5)。
|
||||
|
||||
### 3.3 `loadControllers` 文件规则
|
||||
|
||||
- 目录内 **`*.js`** 或 **`*_controller.js`** 会加载;其他文件忽略。
|
||||
- 每个文件 `require` 后的导出对象由 **`_parseController`** 解析。
|
||||
|
||||
### 3.4 导出键与处理器
|
||||
|
||||
- 键必须匹配 **`/^(GET|POST)\s+(.+)$/`**;否则忽略。
|
||||
- 处理器须为 **function**;签名 **`async (ctx, next) => {}`**,按需 **`await next()`**(系统控制器多为 **`async (ctx) =>`**)。
|
||||
- 注册路径:**`routePrefix`(来自 apiPaths) + 键中 path 段**;系统侧 **`routePrefix` 为 `/admin_api`**。
|
||||
|
||||
---
|
||||
|
||||
## 4. `addRoutes(prefix, routes)`(仅 `beforeInitApi` 内)
|
||||
|
||||
- **`routes`** 须为对象;否则 warn 并 return。
|
||||
- 对每个键:非 function 跳过;正则提取 **method** 与 **path**;最终 **`path: prefix ? prefix + match[2] : match[2]`**,**`module: 'custom'`**,push 到 **`this.customRoutes`**。
|
||||
- **`prefix` 为空字符串**时路径为键内裸 path(常用于与文档路径对齐)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 认证、白名单与 `apiPaths` 匹配(`baseRequest.js`)
|
||||
|
||||
### 5.1 日志
|
||||
|
||||
- 进入中间件后:**`console.log('Process API', method, path)`** — **仅 path**,**不打印 query**(防敏感信息)。
|
||||
|
||||
### 5.2 白名单
|
||||
|
||||
- **`defaultAllowUrls`**:`/login`、`/register`、`/health`、`/docs`、`/swagger.json` 等(以源码为准)。
|
||||
- **`allowUrls`**:`[...defaultAllowUrls, ...(config.allowUrls || [])]`。
|
||||
- 匹配:**`ctx.request.path.indexOf(url) > -1`**(**子串**)。**禁止**配置过宽片段;新增白名单须为**明确路径片段**。
|
||||
|
||||
### 5.3 需鉴权时的 `apiPaths` 匹配
|
||||
|
||||
- 遍历 **`config.apiPaths`**:取 **`prefix`**(对象)或空(字符串形式无 prefix)。
|
||||
- 条件:**`ctx.request.path.indexOf(prefix + '/') > -1`**。
|
||||
- **注意**:`prefix` 建议带前导 **`/`** 且**无尾斜杠**;否则与业务 path 拼接易不匹配。
|
||||
- 命中后:**`authType === 'admin'`** → **`ctx.getAdminUserId()`** 非 0 视为通过;否则 **`ctx.getPappletUserId()`**。
|
||||
- 任一条 `apiPaths` 命中且 userId 有效 → **`next()`**;否则 **`ctx.tokenFail()`**。
|
||||
|
||||
### 5.4 `ctx.getPappletUserId`(小程序)
|
||||
|
||||
- 测试:**`ctx.request.query.userIdTest45`** 优先。
|
||||
- 否则解析头 **`applet-token`**。
|
||||
|
||||
---
|
||||
|
||||
## 6. Context 扩展详解
|
||||
|
||||
### 6.1 `ctx.get(id)`
|
||||
|
||||
- 合并 **`ctx.request.query`** 与 **`ctx.request.body`** 到一对象再取值。
|
||||
- 若合并结果为 **string**,会 **`JSON.parse`**(异常未在片段中捕获时需知悉风险)。
|
||||
- 取值:**`obj[id] === undefined ? "" : obj[id]`**(缺省为 **空字符串**)。
|
||||
|
||||
### 6.2 `ctx.getBody()`
|
||||
|
||||
- **`Object.assign({}, ctx.body, ctx.request.body)`**;若整体为 string 则 **`JSON.parse`**。
|
||||
- **不含 query**;写操作、JSON 体优先。
|
||||
|
||||
### 6.3 `ctx.getQuery()`
|
||||
|
||||
- 返回 **`ctx.request.query`**。
|
||||
|
||||
### 6.4 `ctx.getAdminUserId()` / `ctx.getAdminTenantId()`
|
||||
|
||||
- 读头 **`admin-token`**,**`tokenService.parse`**。
|
||||
- **租户**:若 payload 无 **`tenant_id`**(或 null/空串)则返回 **默认 `1`**;解析失败或无效为 **`0`**(与「未登录」区分以业务为准,源码为数字返回值)。
|
||||
|
||||
### 6.5 `ctx.getPageSize()`
|
||||
|
||||
- 读 **`ctx.get("pageOption")`**;若为字符串则 **`JSON.parse`**。
|
||||
- 返回 **`{ limit: pageSize || 20, offset: pageSize * (page - 1) }`**(**`page` 默认 1**)。
|
||||
|
||||
### 6.6 `ctx.getOrder(key)`
|
||||
|
||||
- **`key`** 默认 **`"order"`**;先 **`ctx.get(key)`**。
|
||||
- 若已是数组则直接返回;否则 **`eval("(" + order + ")")`** — **有注入风险**;新代码应避免把不可信字符串传入,或改为 JSON.parse + 校验。
|
||||
|
||||
### 6.7 `ctx.getIp()`
|
||||
|
||||
- **`x-forwarded-for`** 取第一段;否则 socket 地址。
|
||||
|
||||
### 6.8 响应辅助
|
||||
|
||||
| 方法 | 行为 |
|
||||
|------|------|
|
||||
| **`ctx.json(code, message, data)`** | `type: application/json`,`body: { code, message, data }` |
|
||||
| **`ctx.success(data, msg)`** | **`ctx.json(0, msg \|\| '请求成功', data)`** |
|
||||
| **`ctx.fail(msg)`** | **`ctx.json(-1, message, {})`**;若存在 **`ctx.errorCallback`** 则先调用 |
|
||||
| **`ctx.tokenFail(data)`** | **`ctx.json(-2, '非法请求,或登录已超时', data)`** |
|
||||
| **`ctx.downFile({ title, rows, cols })`** | CSV,**UTF-8 BOM**,`Content-Type: text/csv; charset=utf-8` |
|
||||
|
||||
### 6.9 中间件错误
|
||||
|
||||
- **`try/catch`** 包裹鉴权逻辑:异常时 **`logsService.ctxError`**,**`return ctx.fail(e.message)`**。
|
||||
|
||||
---
|
||||
|
||||
## 7. `start(port)`
|
||||
|
||||
- 若 **`!apiInitialized`**:**抛错** `API not initialized. Call initApi() first.`
|
||||
- **`this.app.listen(port)`** 返回 server。
|
||||
- **定时任务、后台循环**等须在 **`start` 成功之后**再启动(与框架规则一致)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 静态资源与上传目录
|
||||
|
||||
- 默认静态目录指向包内 **`../upload`**;业务部署若需对外提供文件,应明确该目录在磁盘上的位置与权限,而非在控制器里随意 `sendFile` 到任意路径。
|
||||
|
||||
---
|
||||
|
||||
## 9. 数据模型:`DatabaseManager.define` 自动注入字段(`node_core/database/db.js`)
|
||||
|
||||
业务在 **`db.define(name, attributes)`**(或经 `ModelManager` 等价入口)里**只写业务字段**即可;框架会在 **`define()` 内统一合并**下列字段与行为,**无需、也不应在每个模型文件里重复声明** `create_time` / `last_modify_time` / `is_delete`(若 `attributes` 里写了同名键,会被框架定义**覆盖**)。
|
||||
|
||||
### 9.1 自动追加的三列
|
||||
|
||||
| 字段 | 类型与默认 | 说明 |
|
||||
|------|------------|------|
|
||||
| **`create_time`** | `DATE`,库默认 `CURRENT_TIMESTAMP`,`allowNull: false` | **创建时间**。`getter` 用 dayjs 格式化为 **`YYYY-MM-DD HH:mm:ss`** 字符串返回;`setter` 接受 `Date` 或可解析字符串。 |
|
||||
| **`last_modify_time`** | `DATE`,库默认 `CURRENT_TIMESTAMP`,`allowNull: false` | **最后修改时间**。读写格式与 `create_time` 相同。 |
|
||||
| **`is_delete`** | `INTEGER(1)`,默认 **`0`**,`allowNull: false` | **软删标记**:`0` 正常,`1` 已删;`is_swagger: false`(可按生成器策略处理)。 |
|
||||
|
||||
### 9.2 `define` 选项与默认作用域
|
||||
|
||||
- **`tableName`**:与模型名 **`name`** 一致(即表名与 `define` 第一个参数相同)。
|
||||
- **`timestamps: false`**:不使用 Sequelize 内置 `createdAt`/`updatedAt`,改由上述 **`create_time` / `last_modify_time`** 维护。
|
||||
- **`defaultScope`**:
|
||||
- **`where: { is_delete: 0 }`** — 普通 **`findAll` / `findOne` / `findByPk`** 等**默认只查未删除行**。
|
||||
- **`attributes: { exclude: ['is_delete'] }`** — 默认查询结果里**不选出 `is_delete` 列**(行仍受 `where` 约束)。
|
||||
- 需要查已删数据或显式带出 **`is_delete`** 时,须使用 **Sequelize 作用域 API**(如 **`Model.unscoped()`** / **`Model.scope(null)`** 等,以项目所用 Sequelize 版本文档为准)**临时去掉 `defaultScope`**,再自行写 **`where`**;业务仓库内若未封装,不要随意全表无 `where` 查询。
|
||||
|
||||
### 9.3 模型钩子(与操作日志、时间戳)
|
||||
|
||||
- **`beforeCreate`**:若传入的 `create_time` / `last_modify_time` 无效则删除让库/默认值处理;未设置则填 **`new Date()`**。
|
||||
- **`afterCreate`**:在排除表之外写 **`sys_log`**「新增」日志;必要时补全空的时间字段。
|
||||
- **`beforeUpdate`**:对非 **`sys_log`** 表强制 **`last_modify_time = new Date()`**;并取更新前一行与当前值对比写 **`sys_log`**「修改」日志(字段 diff 时**忽略** `create_time` / `last_modify_time` 的变化展示规则见 `_logEditField`)。
|
||||
- **`afterDestroy`**:写 **`sys_log`**「删除」日志(若业务走物理 `destroy`);**业务侧软删推荐**与系统控制器一致:使用 **`update({ is_delete: 1 }, { where: { id, is_delete: 0 } })`**,而不是依赖 `destroy()`。
|
||||
|
||||
### 9.4 列表与统计查询约定
|
||||
|
||||
- **列表/详情**:在 `defaultScope` 下已隐含 **`is_delete: 0`**;若手写 **`raw: true`** 或复杂 **`include`**,仍应显式带上 **`is_delete: 0`**(或与产品确认的可删数据范围),避免误查出已删行。
|
||||
- **Swagger / 文档**:框架侧会把 **`create_time` / `last_modify_time` / `is_delete`** 纳入 schema 相关约定(见 **`services/swagger.js`** 等),与表结构保持一致即可。
|
||||
|
||||
---
|
||||
|
||||
## 10. 控制器编写规范(与既有严格条目一致)
|
||||
|
||||
- **入参**:**`snake_case`**;**禁止** `body.a ?? body.b` 双字段兼容。
|
||||
- **异常**:**不要**在控制器外层包一层「万能 try/catch」吞栈;业务分支用 **`ctx.fail`**;未捕获异常交给全局(若项目有统一错误中间件)。
|
||||
- **`if`**:**必须**使用大括号,**禁止** `if (cond) return ctx.fail(...);` 单行无括号写法。
|
||||
- **模型/服务**:**`Framework.getModels()`**、**`Framework.getServices()`**(或 **`Framework.getInstance()`**)须在 **`init` 完成之后**调用。
|
||||
|
||||
---
|
||||
|
||||
## 11. 配置校验摘要(`_validateConfig`)
|
||||
|
||||
| 配置 | 严重级别 |
|
||||
|------|-----------|
|
||||
| 缺 `db` / 缺 `host|username|password|database|dialect` | **error** |
|
||||
| `apiPaths` 非非空数组 / 项缺 `path` | **error** |
|
||||
| 缺 `baseUrl` | **warning**(Swagger 等可能异常) |
|
||||
| `allowUrls` 非数组 | **warning** |
|
||||
| `redis` 缺 host/port | **warning** |
|
||||
|
||||
---
|
||||
|
||||
## 12. 提交前自检(后端)
|
||||
|
||||
- [ ] 新增路由是否在 **`beforeInitApi`** 内 **`addRoutes`** 或业务控制器导出 **`GET`/`POST`** 键?
|
||||
- [ ] **`apiPaths`** 的 **`prefix`** 是否与 **`indexOf(prefix + '/')`** 匹配方式一致?
|
||||
- [ ] **`allowUrls`** 是否尽量短且明确、避免误放行?
|
||||
- [ ] 读 body 是否优先 **`ctx.getBody()`**,避免与 query 混用?
|
||||
- [ ] 是否避免 **`ctx.getOrder`** 处理不可信字符串?
|
||||
- [ ] 响应是否统一 **`ctx.success` / `ctx.fail` / `ctx.tokenFail`**?
|
||||
- [ ] 控制器是否无万能 try/catch、且 **if 均带大括号**?
|
||||
- [ ] 列表/更新/删除是否默认带上 **`is_delete: 0`**(或与 `defaultScope` 一致),软删是否用 **`update({ is_delete: 1 })`**?
|
||||
Reference in New Issue
Block a user