diff --git a/开发者文档/04-前端开发/01-前端工程结构与协作边界.md b/开发者文档/04-前端开发/01-前端工程结构与协作边界.md
index 0879ecd..2fb7bfc 100644
--- a/开发者文档/04-前端开发/01-前端工程结构与协作边界.md
+++ b/开发者文档/04-前端开发/01-前端工程结构与协作边界.md
@@ -1,43 +1,266 @@
# 01-前端工程结构与协作边界
-本系统 Web 管理端基于 `Vue3 + TypeScript + Vben Admin`,采用 `pnpm monorepo` 架构进行组织。
-前端团队的协作核心不仅是“画页面”,而是**严守应用层与共享层的依赖边界,以及与后端业务域的严格对齐**。
+本文档定义 AIOT Web 管理端(`yudao-ui-admin-vben`)的工程架构、目录组织原则,以及应用层与共享层的依赖红线。所有前端开发人员必须在提代码前确认:这段代码应该放在哪个目录。
-## 一、工程目录架构与红线
+---
-### 1. `apps` 应用层
-应用层包含实际可部署的终端应用:
-- `web-antd`
-- `web-ele`
-- `web-naive`
-- `web-tdesign`
-(具体启用的 UI 框架包视项目配置而定)
+## 一、技术栈与工程架构
-**红线规定:**
-- **应用层只能向下依赖**:`apps` 里的代码可以依赖 `packages/*` 里的代码,但 `packages` 里的代码绝对不能反向依赖 `apps` 里的任何东西。
+### 1.1 核心技术栈
-### 2. `packages` 共享层
-共享层是沉淀业务无关的基础组件和工具的地方:
-- `components`:通用 UI 组件(如自定义 Table、Upload)。
-- `utils`:纯函数工具类(日期格式化、树结构转换)。
-- `types`:全局 TypeScript 接口定义。
-- `stores`:全局状态(不包含业务状态)。
+| 层级 | 技术 | 版本 | 用途 |
+|------|------|------|------|
+| 框架 | Vue 3 | 3.4+ | 渐进式前端框架 |
+| 语言 | TypeScript | 5.0+ | 类型安全 |
+| 构建 | Vite | 5.0+ | 快速构建与热更新 |
+| UI 库 | Ant Design Vue | 4.x | 组件库(当前项目选用) |
+| 状态 | Pinia | 2.1+ | 全局状态管理 |
+| 路由 | Vue Router | 4.x | 单页应用路由 |
+| HTTP | Axios | 1.6+ | 网络请求 |
+| 包管理 | pnpm | 8.x | monorepo 管理 |
-**红线规定:**
-- **Dumb 组件隔离**:`packages/components` 下的组件必须是“Dumb Component(木偶组件)”。它们只通过 `props` 接收数据,通过 `emits` 抛出事件。
-- **禁止网络请求**:共享层组件**严禁直接发起 Axios 请求**,也**严禁耦合 Pinia 状态**。如果一个组件自己去调了后端的 `/api/v1/xxx`,它就必须被移出 `packages`,放到 `apps` 的对应 `views/` 目录下!
+### 1.2 Monorepo 架构
-## 二、业务视图分域(Views)
-
-前端的 `views` 目录,禁止按照“列表页、详情页、表单页”这种视觉逻辑平铺。
-**必须与后端的微服务领域保持 1:1 对齐**:
+项目采用 pnpm workspace 组织的 monorepo 结构:
```
+yudao-ui-admin-vben/
+├── apps/ # 应用层(可部署的终端应用)
+│ └── web-antd/ # Ant Design Vue 版本的管理后台
+├── packages/ # 共享层(业务无关的基础能力)
+│ ├── @core/ # 核心基础(不依赖 UI 框架)
+│ │ ├── base/ # 基础工具、常量
+│ │ ├── components/ # 无 UI 依赖的通用组件
+│ │ └── composables/ # 通用组合式函数
+│ ├── @ui/ # UI 相关(依赖 Ant Design Vue)
+│ │ ├── components/ # UI 组件封装
+│ │ └── styles/ # 主题与样式变量
+│ ├── @utils/ # 工具函数
+│ └── @types/ # 全局类型定义
+└── internal/ # 工程化配置
+ ├── vite-config/ # 共享 Vite 配置
+ └── ts-config/ # 共享 TypeScript 配置
+```
+
+---
+
+## 二、应用层(apps/)规范
+
+### 2.1 目录结构
+
+```
+apps/web-antd/
+├── src/
+│ ├── api/ # API 封装(按业务域组织)
+│ │ ├── system/ # 系统管理相关接口
+│ │ ├── ops/ # Ops 工单相关接口
+│ │ └── iot/ # IoT 设备相关接口
+│ ├── assets/ # 静态资源(图片、字体)
+│ ├── components/ # 业务组件(非通用)
+│ ├── composables/ # 业务组合式函数
+│ ├── directives/ # 自定义指令(如 v-hasPermi)
+│ ├── hooks/ # 业务 Hooks
+│ ├── layouts/ # 布局组件
+│ ├── router/ # 路由配置
+│ ├── stores/ # Pinia Store(按业务域组织)
+│ │ ├── modules/
+│ │ │ ├── user.ts # 用户状态
+│ │ │ ├── dict.ts # 字典数据
+│ │ │ └── permission.ts # 权限状态
+│ ├── styles/ # 样式文件
+│ ├── utils/ # 业务工具函数
+│ ├── views/ # 页面视图(核心业务代码)
+│ │ ├── system/ # 系统管理模块
+│ │ ├── ops/ # Ops 工单模块
+│ │ │ ├── cleaner/ # 保洁管理
+│ │ │ ├── inspection/ # 巡检管理
+│ │ │ └── security/ # 安保管理
+│ │ └── iot/ # IoT 设备模块
+│ │ ├── device/ # 设备管理
+│ │ ├── thing-model/# 物模型管理
+│ │ └── rule/ # 规则引擎
+│ ├── App.vue
+│ └── main.ts
+├── package.json
+└── vite.config.ts
+```
+
+### 2.2 Views 业务分域(关键红线)
+
+**必须与后端微服务模块保持 1:1 对齐**:
+
+| 后端模块 | 前端 views 目录 | 说明 |
+|----------|----------------|------|
+| `module-system` | `views/system/` | 用户、角色、菜单、字典 |
+| `module-infra` | `views/infra/` | 代码生成、定时任务、文件管理 |
+| `module-ops` | `views/ops/` | 工单、巡检、保洁、安保 |
+| `module-iot` | `views/iot/` | 设备、物模型、规则引擎 |
+
+**禁止按页面类型平铺**:
+```
+# ❌ 错误:按页面类型组织
views/
- ├── system/ # 对应 module-system(用户、角色、字典)
- ├── infra/ # 对应 module-infra(定时任务、代码生成)
- ├── ops/ # 对应 module-ops(工单、巡检、排班)
- └── iot/ # 对应 module-iot(设备监控、物模型、规则引擎)
+ ├── list/ # 所有列表页(错误!)
+ ├── form/ # 所有表单页(错误!)
+ └── detail/ # 所有详情页(错误!)
+
+# ✅ 正确:按业务域组织
+views/
+ ├── ops/
+ │ ├── cleaner/
+ │ │ ├── index.vue # 列表页
+ │ │ ├── detail.vue # 详情页
+ │ │ └── form.vue # 表单页
```
-**好处**:当后端 `module-ops` 发生 API 变更或状态机修改时,前端开发者可以明确知道该去改 `views/ops/`,缩小爆炸半径。
\ No newline at end of file
+**好处**:当后端 `module-ops` 发生 API 变更或状态机修改时,前端开发者可以明确知道该去改 `views/ops/`,缩小爆炸半径。
+
+---
+
+## 三、共享层(packages/)规范
+
+### 3.1 分层原则
+
+| 包名 | 依赖范围 | 用途 | 示例 |
+|------|---------|------|------|
+| `@core/base` | 无外部依赖 | 最基础的工具、常量 | `isEmpty`, `deepClone` |
+| `@core/composables` | Vue | 通用组合式函数 | `usePagination`, `useModal` |
+| `@core/components` | Vue | 无 UI 依赖的通用组件 | `Icon`, `SvgIcon` |
+| `@ui/components` | Vue + Ant Design Vue | UI 组件封装 | `BasicTable`, `BasicForm` |
+| `@ui/styles` | 无 | 主题变量、全局样式 | CSS 变量定义 |
+| `@utils` | 视功能而定 | 工具函数集合 | `dateUtil`, `treeUtil` |
+| `@types` | TypeScript | 全局类型定义 | `GlobalEnv`, `RouteRecord` |
+
+### 3.2 关键红线:Dumb Component 原则
+
+**`packages/` 下的组件必须是 Dumb Component(木偶组件)**:
+
+```vue
+
+
+
+
+
+
+
+```
+
+**禁止在共享层组件中**:
+- 直接发起 Axios 请求
+- 直接访问 Pinia Store
+- 硬编码业务逻辑
+
+```vue
+
+
+
+```
+
+**违规处理**:如果需要在组件内调接口或访问 Store,该组件必须移出 `packages/`,放到 `apps/web-antd/src/components/` 或 `views/` 下。
+
+---
+
+## 四、依赖红线检查
+
+### 4.1 依赖方向
+
+```
+apps/ ──depends──→ packages/
+ ↑ ↑
+ │ │
+ └──── forbidden ←────┘
+```
+
+- `apps` 可以依赖 `packages`
+- `packages` 绝对不能依赖 `apps`
+- `packages` 之间按层级依赖(`@core` → `@ui`)
+
+### 4.2 检查命令
+
+```bash
+# 检查是否存在循环依赖或违规依赖
+pnpm m ls
+
+# 检查特定包的依赖树
+pnpm why
+
+# 检查是否存在重复依赖
+pnpm dedupe --check
+```
+
+### 4.3 代码检查(ESLint)
+
+项目配置了 ESLint 规则禁止违规导入:
+
+```javascript
+// .eslintrc.js
+module.exports = {
+ rules: {
+ // 禁止从 apps 导入到 packages
+ 'no-restricted-imports': ['error', {
+ patterns: [{
+ group: ['@/apps/**'],
+ message: 'packages 不能依赖 apps 中的代码'
+ }]
+ }]
+ }
+}
+```
+
+---
+
+## 五、新增代码的归属决策
+
+如果不确定新代码该放哪个目录,按以下决策树:
+
+```
+这段代码是否只服务于特定业务页面?
+├── 是 → 放在 apps/web-antd/src/views/对应业务域/
+└── 否 → 这段代码是否依赖 Ant Design Vue?
+ ├── 是 → 放在 packages/@ui/
+ └── 否 → 这段代码是否依赖 Vue?
+ ├── 是 → 放在 packages/@core/
+ └── 否 → 放在 packages/@utils/ 或 packages/@types/
+```
+
+---
+
+## 六、相关文档
+
+- [02-API交互与状态管理规范.md](./02-API交互与状态管理规范.md) - API 封装与 Pinia 使用
+- [03-RBAC权限控制与开发规范.md](./03-RBAC权限控制与开发规范.md) - 权限指令与动态路由
+- [04-常见坑点与调试指南.md](./04-常见坑点与调试指南.md) - 踩坑记录与调试技巧
diff --git a/开发者文档/04-前端开发/02-API 交互与状态管理规范.md b/开发者文档/04-前端开发/02-API 交互与状态管理规范.md
new file mode 100644
index 0000000..fc54e21
--- /dev/null
+++ b/开发者文档/04-前端开发/02-API 交互与状态管理规范.md
@@ -0,0 +1,1025 @@
+# 02-API 交互与状态管理规范
+
+本文档定义前端与后端 API 的交互规范、TypeScript 类型契约、Pinia 状态管理的使用原则,以及异常处理的统一策略。
+
+---
+
+## 一、HTTP 请求封装
+
+### 1.1 Axios 实例配置
+
+```typescript
+// utils/http/axios/index.ts
+import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios';
+import { useUserStore } from '@/stores/modules/user';
+import { Message, Modal } from 'ant-design-vue';
+import { resultEnum } from '@/enums/httpEnum';
+
+// 创建 Axios 实例
+const axiosInstance = axios.create({
+ baseURL: import.meta.env.VITE_GLOB_API_URL,
+ timeout: 30000,
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8'
+ }
+});
+
+// 请求拦截器
+axiosInstance.interceptors.request.use((config) => {
+ const userStore = useUserStore();
+ const token = userStore.token;
+
+ // 添加认证头
+ if (token) {
+ config.headers['Authorization'] = `Bearer ${token}`;
+ }
+
+ // 添加租户 ID(多租户场景)
+ if (userStore.tenantId) {
+ config.headers['tenant-id'] = userStore.tenantId;
+ }
+
+ return config;
+}, (error) => {
+ return Promise.reject(error);
+});
+
+// 响应拦截器
+axiosInstance.interceptors.response.use(
+ (response) => {
+ const res = response.data;
+
+ // 二进制文件直接返回
+ if (response.config.responseType === 'blob' || response.config.responseType === 'arraybuffer') {
+ return response;
+ }
+
+ // 业务错误处理
+ if (res.code !== resultEnum.SUCCESS.code) {
+ const message = res.msg || res.message || '请求失败';
+
+ // 401: 未授权,清除 token 并跳转登录
+ if (res.code === resultEnum.UNAUTHORIZED.code) {
+ const userStore = useUserStore();
+ Modal.error({
+ title: '登录已过期',
+ content: '请重新登录',
+ onOk: () => {
+ userStore.logout();
+ window.location.href = '/login';
+ }
+ });
+ }
+ // 403: 无权限
+ else if (res.code === resultEnum.FORBIDDEN.code) {
+ Message.error('无访问权限');
+ }
+ // 其他业务错误
+ else {
+ Message.error(message);
+ }
+
+ return Promise.reject(new Error(message));
+ }
+
+ return res;
+ },
+ (error) => {
+ // HTTP 错误处理
+ let message = '网络异常,请稍后重试';
+
+ if (error.response) {
+ switch (error.response.status) {
+ case 400:
+ message = '请求参数错误';
+ break;
+ case 401:
+ message = '未授权,请重新登录';
+ break;
+ case 403:
+ message = '拒绝访问';
+ break;
+ case 404:
+ message = '请求地址不存在';
+ break;
+ case 500:
+ message = '服务器内部错误';
+ break;
+ case 502:
+ message = '网关错误';
+ break;
+ case 503:
+ message = '服务不可用';
+ break;
+ case 504:
+ message = '网关超时';
+ break;
+ default:
+ message = `连接错误${error.response.status}`;
+ }
+ } else if (error.request) {
+ message = '网络异常,请检查网络连接';
+ }
+
+ Message.error(message);
+ return Promise.reject(error);
+ }
+);
+
+// 封装请求方法
+class Request {
+ private instance: AxiosInstance;
+
+ constructor(instance: AxiosInstance) {
+ this.instance = instance;
+ }
+
+ get(config: AxiosRequestConfig): Promise {
+ return this.instance.request({ ...config, method: 'GET' });
+ }
+
+ post(config: AxiosRequestConfig): Promise {
+ return this.instance.request({ ...config, method: 'POST' });
+ }
+
+ put(config: AxiosRequestConfig): Promise {
+ return this.instance.request({ ...config, method: 'PUT' });
+ }
+
+ delete(config: AxiosRequestConfig): Promise {
+ return this.instance.request({ ...config, method: 'DELETE' });
+ }
+
+ upload(config: AxiosRequestConfig): Promise {
+ return this.instance.request({
+ ...config,
+ method: 'POST',
+ headers: { 'Content-Type': 'multipart/form-data' }
+ });
+ }
+
+ download(config: AxiosRequestConfig): Promise {
+ return this.instance.request({
+ ...config,
+ method: 'GET',
+ responseType: 'blob'
+ });
+ }
+}
+
+export const request = new Request(axiosInstance);
+export default axiosInstance;
+```
+
+---
+
+## 二、API 模块化管理
+
+### 2.1 目录结构
+
+API 按业务域组织,与后端微服务模块对齐:
+
+```
+src/api/
+├── system/ # 系统管理(用户、角色、菜单)
+│ ├── user.ts
+│ ├── role.ts
+│ ├── menu.ts
+│ ├── dept.ts
+│ └── dict.ts
+├── ops/ # Ops 工单(保洁、安保、巡检)
+│ ├── work-order.ts
+│ ├── cleaner.ts
+│ ├── security.ts
+│ ├── inspection.ts
+│ └── queue.ts
+├── iot/ # IoT 设备
+│ ├── device.ts
+│ ├── thing-model.ts
+│ ├── rule.ts
+│ └── badge.ts
+├── infra/ # 基础设施(文件、定时任务)
+│ ├── file.ts
+│ └── job.ts
+└── types/ # 公共类型定义
+ └── index.ts
+```
+
+### 2.2 API 文件规范
+
+每个 API 模块文件包含:
+1. 请求参数类型定义(ReqVO)
+2. 响应数据类型定义(ResVO / DO)
+3. 请求方法封装
+
+```typescript
+// api/ops/work-order.ts
+import { request } from '@/utils/http/axios';
+import type { PageResult } from '@/types';
+
+// ==================== 类型定义 ====================
+
+/**
+ * 工单列表请求参数
+ */
+export interface WorkOrderListReqVO {
+ pageNum: number;
+ pageSize: number;
+ orderNo?: string; // 工单号
+ type?: number; // 工单类型(1=保洁,2=安保,3=维修)
+ status?: number; // 工单状态
+ executorId?: number; // 执行人 ID
+ createTime?: [string, string]; // 创建时间范围
+}
+
+/**
+ * 工单详情响应
+ */
+export interface WorkOrderDetailResVO {
+ id: number;
+ orderNo: string;
+ type: number;
+ typeName: string;
+ status: number;
+ statusName: string;
+ priority: number;
+ priorityName: string;
+ title: string;
+ description: string;
+ executorId?: number;
+ executorName?: string;
+ location?: string;
+ locationAddress?: string;
+ beaconMac?: string;
+ expectedFinishTime?: number;
+ actualFinishTime?: number;
+ createTime: number;
+ creatorName: string;
+ images: string[];
+}
+
+/**
+ * 创建工单请求
+ */
+export interface WorkOrderCreateReqVO {
+ type: number;
+ title: string;
+ description: string;
+ priority?: number;
+ location?: string;
+ locationAddress?: string;
+ expectedFinishTime?: number;
+ imageFiles?: File[];
+}
+
+/**
+ * 工单状态变更请求
+ */
+export interface WorkOrderStatusChangeReqVO {
+ status: number;
+ remark?: string;
+ images?: string[];
+}
+
+// ==================== API 方法 ====================
+
+/**
+ * 获取工单列表
+ */
+export function getWorkOrderList(params: WorkOrderListReqVO) {
+ return request.get>({
+ url: '/ops/work-order/list',
+ params
+ });
+}
+
+/**
+ * 获取工单详情
+ */
+export function getWorkOrderDetail(id: number) {
+ return request.get({
+ url: `/ops/work-order/${id}`
+ });
+}
+
+/**
+ * 创建工单
+ */
+export function createWorkOrder(data: WorkOrderCreateReqVO) {
+ return request.post({
+ url: '/ops/work-order/create',
+ data
+ });
+}
+
+/**
+ * 更新工单
+ */
+export function updateWorkOrder(id: number, data: Partial) {
+ return request.put({
+ url: `/ops/work-order/${id}/update`,
+ data
+ });
+}
+
+/**
+ * 删除工单
+ */
+export function deleteWorkOrder(id: number) {
+ return request.delete({
+ url: `/ops/work-order/${id}/delete`,
+ params: { id }
+ });
+}
+
+/**
+ * 工单状态变更(接单/完工/审核等)
+ */
+export function changeWorkOrderStatus(id: number, data: WorkOrderStatusChangeReqVO) {
+ return request.put({
+ url: `/ops/work-order/${id}/status`,
+ data
+ });
+}
+
+/**
+ * 工单派单
+ */
+export function dispatchWorkOrder(orderId: number, executorId: number) {
+ return request.post({
+ url: `/ops/work-order/${orderId}/dispatch`,
+ data: { executorId }
+ });
+}
+
+/**
+ * 导出工单列表
+ */
+export function exportWorkOrderList(params: WorkOrderListReqVO) {
+ return request.download({
+ url: '/ops/work-order/export-excel',
+ params
+ });
+}
+```
+
+### 2.3 公共类型定义
+
+```typescript
+// types/index.ts
+
+/**
+ * 分页响应
+ */
+export interface PageResult {
+ list: T[];
+ total: number;
+ pageNum: number;
+ pageSize: number;
+ pages: number;
+}
+
+/**
+ * 通用响应
+ */
+export interface ApiResponse {
+ code: number;
+ msg: string;
+ data: T;
+}
+
+/**
+ * 时间范围选择
+ */
+export type DateRange = [string, string] | null;
+
+/**
+ * 文件上传响应
+ */
+export interface UploadResult {
+ url: string;
+ name: string;
+ size: number;
+}
+```
+
+---
+
+## 三、页面调用规范
+
+### 3.1 列表页标准写法
+
+```vue
+
+
+
+
+
+
+
+
+
+
+ 保洁
+ 安保
+ 维修
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+ 查询
+ 重置
+
+
+
+
+
+
+
+
+ 新增工单
+
+ 导出
+
+
+
+
+
+
+
+
+
+
+ 详情
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+```
+
+---
+
+## 四、Pinia 状态管理
+
+### 4.1 什么数据应该进 Store
+
+**应该进 Store 的数据**:
+- ✅ 登录信息(token、用户信息、权限列表)
+- ✅ 全局配置(系统设置、主题配置)
+- ✅ 字典数据(工单类型、状态枚举等)
+- ✅ 动态路由和菜单
+- ✅ 跨页面共享的业务状态
+
+**不应该进 Store 的数据**:
+- ❌ 列表页的翻页参数(`pageNum`、`pageSize`)
+- ❌ 表单的中间输入状态
+- ❌ 单个页面的详情数据
+- ❌ 临时计算结果
+
+**判断标准**:刷新页面后是否还需要保留?多个页面是否共享?
+
+### 4.2 用户 Store
+
+```typescript
+// stores/modules/user.ts
+import { defineStore } from 'pinia';
+import { ref, computed } from 'vue';
+import { loginApi, getUserInfoApi, logoutApi } from '@/api/system/auth';
+import { setToken, removeToken, getToken } from '@/utils/auth';
+
+export interface UserInfo {
+ id: number;
+ username: string;
+ nickname: string;
+ avatar: string;
+ email: string;
+ phone: string;
+ sex: number;
+ deptId: number;
+ deptName: string;
+}
+
+export const useUserStore = defineStore('user', () => {
+ // State
+ const token = ref(getToken() || '');
+ const user = ref(null);
+ const permissions = ref([]);
+ const roles = ref([]);
+ const dataScope = ref('ONLY_SELF');
+
+ // Getters
+ const isLoggedIn = computed(() => !!token.value);
+ const userName = computed(() => user.value?.nickname || user.value?.username || '');
+ const userAvatar = computed(() => user.value?.avatar || '');
+
+ // Actions
+ /**
+ * 登录
+ */
+ async function login(username: string, password: string) {
+ const result = await loginApi({ username, password, grantType: 'password' });
+
+ token.value = result.accessToken;
+ setToken(result.accessToken);
+
+ // 登录成功后获取用户信息
+ await getUserInfo();
+
+ return result;
+ }
+
+ /**
+ * 获取用户信息
+ */
+ async function getUserInfo() {
+ const result = await getUserInfoApi();
+
+ user.value = result.user;
+ permissions.value = result.permissions;
+ roles.value = result.roles;
+ dataScope.value = result.dataScope;
+ }
+
+ /**
+ * 登出
+ */
+ async function logout() {
+ try {
+ await logoutApi();
+ } catch {
+ // 忽略登出接口错误
+ } finally {
+ resetState();
+ }
+ }
+
+ /**
+ * 重置状态
+ */
+ function resetState() {
+ token.value = '';
+ user.value = null;
+ permissions.value = [];
+ roles.value = [];
+ dataScope.value = 'ONLY_SELF';
+ removeToken();
+ }
+
+ return {
+ token,
+ user,
+ permissions,
+ roles,
+ dataScope,
+ isLoggedIn,
+ userName,
+ userAvatar,
+ login,
+ getUserInfo,
+ logout,
+ resetState
+ };
+});
+```
+
+### 4.3 字典 Store
+
+```typescript
+// stores/modules/dict.ts
+import { defineStore } from 'pinia';
+import { ref, computed } from 'vue';
+import { getDictDataApi } from '@/api/system/dict';
+
+export interface DictOption {
+ label: string;
+ value: string | number;
+ color?: string;
+}
+
+export const useDictStore = defineStore('dict', () => {
+ // State: 字典数据缓存
+ const dictMap = ref>({});
+
+ // Getters
+ const getDict = computed(() => (type: string) => dictMap.value[type] || []);
+
+ // Actions
+ /**
+ * 加载字典
+ * @param types 字典类型数组,如 ['ops_work_order_type', 'ops_work_order_status']
+ */
+ async function loadDict(types: string[]) {
+ const needLoadTypes = types.filter(type => !dictMap.value[type]);
+
+ if (needLoadTypes.length === 0) return;
+
+ try {
+ const promises = needLoadTypes.map(type => getDictDataApi(type));
+ const results = await Promise.all(promises);
+
+ results.forEach((result, index) => {
+ const type = needLoadTypes[index];
+ dictMap.value[type] = result.map(item => ({
+ label: item.label,
+ value: item.value,
+ color: item.colorType
+ }));
+ });
+ } catch (error) {
+ console.error('[DictStore] 加载字典失败:', error);
+ }
+ }
+
+ /**
+ * 获取字典标签
+ * @param type 字典类型
+ * @param value 字典值
+ */
+ function getDictLabel(type: string, value: string | number): string {
+ const dict = dictMap.value[type] || [];
+ const option = dict.find(item => item.value === value);
+ return option?.label || String(value);
+ }
+
+ return {
+ dictMap,
+ getDict,
+ loadDict,
+ getDictLabel
+ };
+});
+```
+
+### 4.4 页面中使用 Store
+
+```vue
+
+```
+
+---
+
+## 五、文件上传
+
+### 5.1 单文件上传
+
+```vue
+
+
+ 选择图片
+
+
+
+
+```
+
+### 5.2 多文件上传(工单图片)
+
+```vue
+
+
+
+
+
+
+
+```
+
+---
+
+## 六、异常处理最佳实践
+
+### 6.1 统一拦截器处理
+
+所有 HTTP 异常已在 Axios 拦截器中统一处理,**页面代码中不需要重复处理**:
+
+```typescript
+// ❌ 错误:每个页面都写一遍错误处理
+try {
+ await getWorkOrderList(params);
+} catch (error) {
+ Message.error('加载失败');
+}
+
+// ✅ 正确:直接调用,错误由拦截器处理
+await getWorkOrderList(params);
+```
+
+### 6.2 需要特殊处理的场景
+
+只有以下情况需要在页面中处理异常:
+
+```typescript
+// 1. 需要静默失败(不提示错误)
+try {
+ await someApi(params);
+} catch {
+ // 静默失败,不做任何提示
+}
+
+// 2. 需要根据错误类型做不同处理
+try {
+ await login(username, password);
+} catch (error: any) {
+ if (error.response?.status === 401) {
+ // 密码错误,清空密码框
+ password.value = '';
+ }
+}
+
+// 3. 需要自定义错误提示
+try {
+ await deleteWorkOrder(id);
+} catch {
+ // 使用自定义提示
+ Modal.success({
+ title: '删除成功',
+ content: '工单已移至回收站'
+ });
+}
+```
+
+---
+
+## 七、相关代码入口
+
+| 模块 | 文件路径 | 说明 |
+|-----|---------|------|
+| Axios 封装 | `src/utils/http/axios/index.ts` | 请求/响应拦截器 |
+| 工单 API | `src/api/ops/work-order.ts` | 工单相关接口 |
+| 用户 Store | `src/stores/modules/user.ts` | 登录态与用户信息 |
+| 字典 Store | `src/stores/modules/dict.ts` | 字典数据缓存 |
+| 权限指令 | `src/directives/permission/index.ts` | `v-hasPermi` |
+| 文件上传 | `src/api/infra/file.ts` | 文件上传接口 |
diff --git a/开发者文档/04-前端开发/02-API交互与状态管理规范.md b/开发者文档/04-前端开发/02-API交互与状态管理规范.md
index b758742..cd47409 100644
--- a/开发者文档/04-前端开发/02-API交互与状态管理规范.md
+++ b/开发者文档/04-前端开发/02-API交互与状态管理规范.md
@@ -1,43 +1,104 @@
# 02-API交互与状态管理规范
-前端对接后端的 API 层是协作矛盾的高发地。本系统通过强类型的 API 封装和严格的状态管理原则来约束开发。
+本文档定义前端与后端 API 的交互规范、TypeScript 契约定义,以及 Pinia 状态管理的使用原则。
+
+---
## 一、API 封装规范
-### 1. 绝对禁止在页面内联请求
-**错误示范(打回重构):**
-```typescript
-// 在 view/ops/TicketList.vue 中
-axios.get('/api/ops/ticket/list').then(...)
+### 1.1 目录结构
+
+```
+api/
+├── system/
+├── ops/
+├── iot/
+└── types/
```
-**正确规范:**
-1. 所有请求必须在 `api/` 目录下按业务域归类集中管理。
-2. 页面中只允许调用 `import { getTicketList } from '@/api/ops/ticket'`。
+### 1.2 完整示例
-### 2. 强制 TypeScript 契约
-- 后端 Swagger/YApi 中更新了接口契约,前端必须在 `api/` 下定义对应的 `ReqVO` 和 `RespVO`。
-- **禁止 AnyScript**:不允许在 API 响应处使用 `any` 糊弄。任何因为 `any` 导致的线上字段报错(如后端改了驼峰命名前端没发现),由前端开发者负全责。
+```typescript
+// api/ops/cleaner.ts
+import { request } from '@/utils/http/axios'
-## 二、Pinia 状态管理瘦身原则
+export interface CleanerListReqVO {
+ pageNum: number
+ pageSize: number
+ cleanerName?: string
+ status?: string
+}
-很多前端项目后期变卡的罪魁祸首是“把所有东西都塞进全局 Store”。
+export interface CleanerWorkOrderVO {
+ id: number
+ orderNo: string
+ cleanerName: string
+ status: string
+ location: string
+}
-### 1. 什么数据可以进 Pinia
-- 登录用户信息、Token。
-- 字典数据(静态常量、枚举值)。
-- 用户动态路由和菜单树。
-- 全局主题与布局配置。
+export const getCleanerList = (params: CleanerListReqVO) => {
+ return request.get({ url: '/ops/cleaner/list', params })
+}
-### 2. 什么数据禁止进 Pinia
-- 列表的翻页数据(`pageNum`, `pageSize`, `total`)。
-- 表单编辑的中间脏状态。
-- 特定页面的业务缓存。
+export const dispatchOrder = (data: { orderId: number; cleanerId: number }) => {
+ return request.post({ url: '/ops/cleaner/dispatch', data })
+}
+```
-**红线**:业务页面的状态闭环必须保留在 Vue 的 Composition API(`ref`, `reactive`)中,页面销毁时数据必须自然回收,禁止污染全局 Store。
+### 1.3 页面调用
-## 三、统一异常与提示拦截
+```vue
+
+```
+
+---
+
+## 二、Pinia 状态管理
+
+### 2.1 可进 Store 的数据
+- 登录信息、Token
+- 字典数据
+- 动态路由
+- 全局主题
+
+### 2.2 禁止进 Store 的数据
+- 列表翻页参数
+- 表单中间状态
+- 业务详情数据
+
+### 2.3 Store 示例
+
+```typescript
+// stores/modules/dict.ts
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+
+export const useDictStore = defineStore('dict', () => {
+ const dictMap = ref({})
+
+ const getDict = computed(() => (type: string) => dictMap.value[type] || [])
+
+ const loadDict = async (types: string[]) => {
+ // 加载字典...
+ }
+
+ return { dictMap, getDict, loadDict }
+})
+```
+
+---
+
+## 三、异常处理
+
+统一在 Axios 拦截器中处理:
+- 业务错误(code !== 0):Message.error 提示
+- 401:清除 Token 跳转登录
+- 禁止在每个页面 catch 中手写 alert
diff --git a/开发者文档/04-前端开发/03-RBAC 权限控制与开发规范.md b/开发者文档/04-前端开发/03-RBAC 权限控制与开发规范.md
new file mode 100644
index 0000000..a2a604a
--- /dev/null
+++ b/开发者文档/04-前端开发/03-RBAC 权限控制与开发规范.md
@@ -0,0 +1,736 @@
+# 03-RBAC 权限控制与开发规范
+
+管理后台的安全性要求极高,权限控制必须做实做细,**严禁"靠隐藏按钮防黑客"**。
+本文档基于 `yudao-ui-admin-vben` 真实代码,阐述前端 RBAC 权限控制的完整实现。
+
+---
+
+## 一、权限体系架构
+
+AIOT 平台的权限控制分为三个层级:
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ 权限控制层级 │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ ┌───────────────┐ 菜单级权限(路由守卫) │
+│ │ 页面访问权 │ ────────────────────── │
+│ │ (Menu) │ 无权限用户无法访问页面,路由不存在 │
+│ └───────────────┘ │
+│ │
+│ ┌───────────────┐ 按钮级权限(指令/组件) │
+│ │ 操作执行权 │ ────────────────────── │
+│ │ (Button) │ v-hasPermi 控制按钮显隐 │
+│ └───────────────┘ │
+│ │
+│ ┌───────────────┐ 数据级权限(接口 + 前端展示) │
+│ │ 数据可见权 │ ────────────────────── │
+│ │ (Data Scope) │ 用户只能看到本部门/本区域的数据 │
+│ └───────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+### 1.1 权限标识命名规范
+
+所有权限标识(Permission String)必须遵循统一格式:
+
+```
+<模块>:<业务>:<操作>
+
+示例:
+ops:work-order:list # 工单列表查询
+ops:work-order:create # 工单创建
+ops:work-order:update # 工单修改
+ops:work-order:delete # 工单删除
+ops:work-order:audit # 工单审核
+ops:work-order:dispatch # 工单派单
+ops:inspection:record # 巡检记录
+iot:device:reboot # 设备重启
+iot:device:config # 设备配置
+system:user:reset-password # 重置密码
+```
+
+**权限标识来源**:后端 Controller 方法上的 `@PreAuthorize` 注解,前端必须与之严格对齐。
+
+```java
+// 后端示例(SecurityUtils.java)
+@RestController
+@RequestMapping("/ops/work-order")
+public class WorkOrderController {
+
+ @PreAuthorize("@ss.hasPermi('ops:work-order:list')")
+ @GetMapping("/list")
+ public PageResult list(...) { ... }
+
+ @PreAuthorize("@ss.hasPermi('ops:work-order:create')")
+ @PostMapping("/create")
+ public Long create(@RequestBody WorkOrderCreateReqVO reqVO) { ... }
+}
+```
+
+---
+
+## 二、动态路由加载流程
+
+系统的左侧菜单树并非前端写死,而是基于用户的角色权限,登录后从后端动态获取并挂载。
+
+### 2.1 路由加载时序图
+
+```mermaid
+sequenceDiagram
+ participant U as 用户
+ participant F as 前端 (Vue Router)
+ participant S as 前端 Store (Pinia)
+ participant B as 后端 API
+
+ U->>F: 访问系统首页 /
+ F->>S: 检查登录态 (token)
+
+ alt 未登录
+ F->>U: 重定向到登录页 /login
+ else 已登录
+ F->>S: 检查是否已加载路由
+ S->>S: 检查 permissionStore.isRouteAdded
+
+ alt 路由已加载
+ S->>F: 直接放行
+ else 路由未加载
+ S->>B: GET /system/menu/get-routers
+ B-->>S: 返回菜单树(按权限过滤)
+
+ S->>S: 将菜单树转换为路由配置
(generateRoutes)
+ S->>F: router.addRoute(routes)
+ S->>S: 标记 isRouteAdded = true
+
+ S->>F: 重新导航到原目标页
+ end
+ end
+```
+
+### 2.2 核心代码实现
+
+```typescript
+// stores/permission.ts
+import { defineStore } from 'pinia';
+import type { RouteRecordRaw } from 'vue-router';
+import { constantRoutes } from '@/router';
+import { getRouters } from '@/api/system/menu';
+import { transformMenuToRoutes } from '@/utils/router-helper';
+
+export const usePermissionStore = defineStore('permission', () => {
+ // State
+ const permissionRoutes = ref([]);
+ const isRouteAdded = ref(false);
+ const sidebarMenus = ref([]);
+
+ // Actions
+ /**
+ * 生成并添加路由
+ * 在登录后或刷新页面时调用
+ */
+ async function generateRoutes() {
+ if (isRouteAdded.value) {
+ return permissionRoutes.value;
+ }
+
+ try {
+ // 1. 从后端获取菜单树(已按当前用户权限过滤)
+ const menuTree = await getRouters();
+
+ // 2. 将菜单树转换为 Vue Router 配置
+ const asyncRoutes = transformMenuToRoutes(menuTree);
+
+ // 3. 合并静态路由(登录页、404 等)
+ permissionRoutes.value = [...constantRoutes, ...asyncRoutes];
+
+ // 4. 生成侧边栏菜单
+ sidebarMenus.value = menuTree;
+
+ // 5. 标记路由已添加(注意:必须在 addRoute 之后)
+ isRouteAdded.value = true;
+
+ return permissionRoutes.value;
+ } catch (error) {
+ console.error('[PermissionStore] 路由加载失败:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * 重置权限状态(登出时调用)
+ */
+ function resetPermission() {
+ permissionRoutes.value = [];
+ isRouteAdded.value = false;
+ sidebarMenus.value = [];
+ }
+
+ return {
+ permissionRoutes,
+ isRouteAdded,
+ sidebarMenus,
+ generateRoutes,
+ resetPermission
+ };
+});
+```
+
+### 2.3 路由守卫(Permission Guard)
+
+```typescript
+// router/index.ts
+import { createRouter, createWebHistory } from 'vue-router';
+import { useUserStore, usePermissionStore } from '@/stores';
+import NProgress from 'nprogress';
+
+const router = createRouter({
+ history: createWebHistory(),
+ routes: constantRoutes // 初始只注册静态路由
+});
+
+// 白名单(无需登录即可访问)
+const whiteList = ['/login', '/register', '/404'];
+
+router.beforeEach(async (to, from, next) => {
+ NProgress.start();
+
+ const userStore = useUserStore();
+ const permissionStore = usePermissionStore();
+ const hasToken = userStore.token;
+
+ if (hasToken) {
+ // 已登录
+ if (to.path === '/login') {
+ // 已登录用户访问登录页,重定向到首页
+ next({ path: '/' });
+ NProgress.done();
+ } else {
+ // 检查路由是否已加载
+ if (permissionStore.isRouteAdded) {
+ // 路由已加载,直接放行
+ next();
+ } else {
+ try {
+ // 路由未加载,生成并添加路由
+ await permissionStore.generateRoutes();
+
+ // 动态添加路由
+ permissionStore.permissionRoutes.forEach(route => {
+ router.addRoute(route);
+ });
+
+ // 重新导航到原目标(确保路由已注册)
+ next({ ...to, replace: true });
+ } catch (error) {
+ // 路由加载失败(token 过期/权限异常)
+ await userStore.logout();
+ next(`/login?redirect=${to.path}`);
+ NProgress.done();
+ }
+ }
+ }
+ } else {
+ // 未登录
+ if (whiteList.includes(to.path)) {
+ // 在白名单内,直接放行
+ next();
+ } else {
+ // 重定向到登录页
+ next(`/login?redirect=${to.path}`);
+ NProgress.done();
+ }
+ }
+});
+
+router.afterEach(() => {
+ NProgress.done();
+});
+
+export default router;
+```
+
+### 2.4 菜单树转路由配置
+
+```typescript
+// utils/router-helper.ts
+import type { RouteRecordRaw } from 'vue-router';
+import type { MenuInfo } from '@/types/system';
+
+/**
+ * 将后端返回的菜单树转换为 Vue Router 配置
+ */
+export function transformMenuToRoutes(menuTree: MenuInfo[]): RouteRecordRaw[] {
+ const routes: RouteRecordRaw[] = [];
+
+ menuTree.forEach(menu => {
+ // 只处理目录和菜单类型的节点
+ if (menu.type === 'dir' || menu.type === 'menu') {
+ const route: RouteRecordRaw = {
+ path: menu.path,
+ name: menu.name,
+ component: loadComponent(menu.component),
+ redirect: menu.redirect,
+ meta: {
+ title: menu.name,
+ icon: menu.icon,
+ hidden: menu.visible === '0', // 1=显示,0=隐藏
+ keepAlive: menu.keepAlive === '1',
+ permission: menu.permission // 权限标识
+ },
+ children: []
+ };
+
+ // 递归处理子菜单
+ if (menu.children && menu.children.length > 0) {
+ route.children = transformMenuToRoutes(menu.children);
+ }
+
+ routes.push(route);
+ }
+ });
+
+ return routes;
+}
+
+/**
+ * 动态加载组件(Vite 语法)
+ */
+function loadComponent(componentPath?: string) {
+ if (!componentPath) {
+ return () => import('@/layouts/default/index.vue');
+ }
+
+ // 支持多种组件路径格式
+ const paths = [
+ `@/views/${componentPath}`,
+ `@/views/${componentPath}/index.vue`,
+ componentPath
+ ];
+
+ for (const path of paths) {
+ try {
+ return () => import(/* @vite-ignore */ path);
+ } catch {
+ continue;
+ }
+ }
+
+ // 默认返回 404 组件
+ console.warn(`[RouterHelper] 组件加载失败:${componentPath}`);
+ return () => import('@/views/error-page/404.vue');
+}
+```
+
+---
+
+## 三、按钮级权限控制
+
+### 3.1 `v-hasPermi` 指令实现
+
+```typescript
+// directives/permission/index.ts
+import type { App, Directive, DirectiveBinding } from 'vue';
+import { useUserStore } from '@/stores/modules/user';
+
+/**
+ * 全局权限指令
+ * 用法:新增
+ */
+const hasPermission: Directive = {
+ mounted(el: HTMLElement, binding: DirectiveBinding) {
+ const { value } = binding;
+
+ if (value) {
+ const requiredPermissions = Array.isArray(value) ? value : [value];
+ const userStore = useUserStore();
+ const allPermissions = userStore.permissions; // 用户拥有的所有权限标识
+
+ // 检查是否拥有任一权限
+ const hasPermission = requiredPermissions.some(permission =>
+ allPermissions.includes(permission)
+ );
+
+ if (!hasPermission) {
+ // 无权限,移除 DOM 元素
+ el.parentNode?.removeChild(el);
+ }
+ } else {
+ throw new Error('[Directive] v-hasPermi 需要传入权限标识数组');
+ }
+ }
+};
+
+/**
+ * 注册全局指令
+ */
+export function setupPermissionDirective(app: App) {
+ app.directive('hasPermi', hasPermission);
+}
+
+export default hasPermission;
+```
+
+### 3.2 `usePermission` Hook
+
+对于不能在模板中使用指令的场景(如动态渲染表格列、JS 逻辑判断),使用 Hook:
+
+```typescript
+// hooks/web/usePermission.ts
+import { useUserStore } from '@/stores/modules/user';
+
+export function usePermission() {
+ const userStore = useUserStore();
+
+ /**
+ * 检查是否拥有指定权限
+ * @param permission 权限标识
+ * @returns 是否拥有权限
+ */
+ function hasPermission(permission: string): boolean {
+ return userStore.permissions.includes(permission);
+ }
+
+ /**
+ * 检查是否拥有任一权限
+ * @param permissions 权限标识数组
+ * @returns 是否拥有任一权限
+ */
+ function hasAnyPermission(permissions: string[]): boolean {
+ return permissions.some(permission =>
+ userStore.permissions.includes(permission)
+ );
+ }
+
+ /**
+ * 检查是否拥有所有权限
+ * @param permissions 权限标识数组
+ * @returns 是否拥有所有权限
+ */
+ function hasAllPermissions(permissions: string[]): boolean {
+ return permissions.every(permission =>
+ userStore.permissions.includes(permission)
+ );
+ }
+
+ return {
+ hasPermission,
+ hasAnyPermission,
+ hasAllPermissions
+ };
+}
+```
+
+### 3.3 使用示例
+
+```vue
+
+
+
+
+ 编辑
+
+
+
+ 审核
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+ 派单
+
+
+
+
+
+
+
+
+
+```
+
+---
+
+## 四、数据权限(Data Scope)
+
+数据权限控制用户能看到哪些数据(如:只能看本部门的工单)。
+**数据权限的核心在后端**,但前端需要配合处理展示逻辑。
+
+### 4.1 后端数据范围类型
+
+```java
+// 后端数据范围枚举(DataScopeEnum)
+public enum DataScopeEnum {
+ ALL, // 全部数据权限
+ CUSTOM_DEPT, // 自定义部门数据权限
+ SINGLE_DEPT, // 本部门数据权限
+ DEPT_AND_CHILD, // 本部门及以下数据权限
+ ONLY_SELF // 仅本人数据权限
+}
+```
+
+### 4.2 前端处理策略
+
+```typescript
+// stores/user.ts
+export const useUserStore = defineStore('user', () => {
+ // State
+ const user = ref(null);
+ const permissions = ref([]);
+ const dataScope = ref('ONLY_SELF');
+ const deptId = ref(null);
+
+ // Actions
+ async function login(username: string, password: string) {
+ const result = await loginApi({ username, password });
+
+ // 保存用户信息
+ user.value = result.user;
+ permissions.value = result.permissions;
+ dataScope.value = result.dataScope;
+ deptId.value = result.deptId;
+
+ // 保存 token
+ token.value = result.token;
+ }
+
+ return {
+ user,
+ permissions,
+ dataScope,
+ deptId,
+ login,
+ // ...
+ };
+});
+```
+
+### 4.3 前端展示适配
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+---
+
+## 五、权限配置同步流程
+
+当后端新增权限时,前后端需要同步更新:
+
+```mermaid
+flowchart TD
+ A[后端开发] -->|1. 新增 Controller 方法 | B[添加@PreAuthorize 注解]
+ B --> C[编写 SQL 插入菜单/按钮记录]
+ C --> D[提交后端代码]
+
+ D --> E{是否需要前端 UI?}
+ E -->|是 | F[前端开发:新增页面/按钮]
+ E -->|否 | G[无需前端改动]
+
+ F --> H[使用 v-hasPermi 绑定权限标识]
+ H --> I[提交前端代码]
+
+ I --> J[测试环境验证]
+ J --> K[生产环境部署]
+ K --> L[管理员分配权限给角色]
+```
+
+### 5.1 后端 SQL 示例
+
+```sql
+-- 新增菜单记录(系统管理 -> 工单管理 -> 工单审核按钮)
+INSERT INTO system_menu (
+ parent_id, name, type, path, component,
+ permission, visible, status
+) VALUES (
+ (SELECT id FROM system_menu WHERE name = '工单管理'), -- 父菜单 ID
+ '工单审核',
+ 'button',
+ '',
+ '',
+ 'ops:work-order:audit', -- 权限标识(必须与后端@PreAuthorize 一致)
+ '1',
+ '0'
+);
+```
+
+### 5.2 前端权限标识对齐检查清单
+
+- [ ] 权限标识字符串与后端 `@PreAuthorize` 注解完全一致
+- [ ] 权限标识已录入 SQL 初始化脚本
+- [ ] 前端按钮使用 `v-hasPermi` 而非 `v-if="role === 'xxx'"`
+- [ ] 新增的菜单/按钮已在后端 `system/menu/get-routers` 返回的树中
+- [ ] 测试账号已分配对应角色并验证权限生效
+
+---
+
+## 六、常见坑点与排障
+
+### 6.1 路由已加载但菜单不显示
+
+**现象**:用户有权限访问页面,但左侧菜单树看不到入口。
+
+**原因**:后端 `system/menu/get-routers` 接口返回的菜单树中,该菜单的 `visible` 字段为 `0`(隐藏)。
+
+**排查**:
+```sql
+-- 检查菜单可见性
+SELECT id, name, visible FROM system_menu WHERE permission = 'ops:work-order:list';
+```
+
+**解决**:将 `visible` 改为 `1`,或在前端路由配置中设置 `meta.hidden = false`。
+
+### 6.2 按钮不显示但接口能调通
+
+**现象**:按钮被 `v-hasPermi` 隐藏,但直接在浏览器调用接口能成功。
+
+**原因**:用户未分配对应权限,但后端接口未加 `@PreAuthorize` 注解。
+
+**排查**:
+1. 检查前端 `userStore.permissions` 是否包含该权限标识
+2. 检查后端 Controller 方法是否有 `@PreAuthorize` 注解
+
+**风险**:这是严重的安全漏洞!前端隐藏只是防君子,后端必须加权限校验。
+
+### 6.3 刷新页面后权限丢失
+
+**现象**:登录正常,刷新页面后路由加载失败,重定向到登录页。
+
+**原因**:Pinia Store 状态在刷新后丢失,但 token 还在 localStorage 中。
+
+**解决**:在 `main.ts` 或路由守卫中,检查 token 存在但 Store 为空时,重新获取用户信息:
+
+```typescript
+// stores/user.ts
+async function initUserInfo() {
+ if (!this.token) return;
+
+ try {
+ const userInfo = await getUserInfoApi();
+ this.user = userInfo.user;
+ this.permissions = userInfo.permissions;
+ this.dataScope = userInfo.dataScope;
+ } catch {
+ // 获取失败,清除 token 并跳转登录
+ await this.logout();
+ }
+}
+```
+
+### 6.4 权限标识大小写问题
+
+**现象**:后端注解是 `'ops:work-order:list'`,前端写成了 `'ops:Work-Order:List'`。
+
+**解决**:权限标识统一使用**小写字母 + 连字符**,在团队规范中明确约定。
+
+---
+
+## 七、相关代码入口
+
+| 模块 | 文件路径 | 说明 |
+|-----|---------|------|
+| 权限 Store | `src/stores/modules/permission.ts` | 动态路由加载 |
+| 用户 Store | `src/stores/modules/user.ts` | 用户信息与权限列表 |
+| 权限指令 | `src/directives/permission/index.ts` | `v-hasPermi` 实现 |
+| 权限 Hook | `src/hooks/web/usePermission.ts` | JS 内权限判断 |
+| 路由守卫 | `src/router/index.ts` | 登录态与路由检查 |
+| 路由工具 | `src/utils/router-helper.ts` | 菜单树转路由配置 |
+| 菜单 API | `src/api/system/menu.ts` | `getRouters()` 接口 |
+
+---
+
+## 八、安全红线
+
+| 红线项 | 说明 | 后果 |
+|-------|------|------|
+| **前端隐藏代替后端校验** | 按钮隐藏不等于接口安全 | 未授权访问 |
+| **硬编码角色名** | `v-if="role === 'admin'"` | 权限变更需发版 |
+| **权限标识不一致** | 前后端字符串对不上 | 权限失效或越权 |
+| **菜单 SQL 漏配置** | 新增权限忘记插菜单记录 | 用户看不到入口 |
+| **刷新后不恢复权限** | Store 丢失未重新获取 | 用户被迫重新登录 |