# 备货单 / 终版列表「全部 / 我的」视图切换 设计 - 日期:2026-08-06 - 分支:feature/crm_pc - 涉及页面:`src/views/crmpc/stocking/stocking_list.vue`、`src/views/crmpc/stocking/final_list.vue` - 涉及接口:`GET /crm/stock/order/list` ## 1. 背景 备货单列表(`stocking_list`,`is_final=0`)与终版列表(`final_list`,`is_final=1`)当前共用同一个列表接口 `GET /crm/stock/order/list`。后端依据请求 token 中的用户 id 过滤数据,即每个登录用户只能看到自己作为业务员的备货单 / 终版。前端请求参数只有 `current / size / is_final / keyword`,无任何范围控制。 业务上存在一个固定特权用户(user_id = 73),需要在「看自己」之外,额外能查看「全公司所有备货单 / 终版」。 ## 2. 需求 - 给 user_id = 73 这一个用户,在两个列表页增加「全部 / 我的」视图切换。 - 选「全部」时,该用户看到全公司数据;选「我的」时,行为同现状(只看自己)。 - 其他用户(非 73)完全不受影响:不显示切换控件,行为同现状。 - 默认视图为「我的」。 ## 3. 设计 ### 3.1 UI - 在两个列表页的工具栏增加一组互斥切换控件「全部 / 我的」(`el-radio-group` button 样式,二选一)。 - 控件位置:工具栏右侧、搜索框左边,两个页面位置保持一致(`final_list` 现有 `toolbar-left` 被注释,不在那里放,统一放右侧,改动更小)。 - 渲染条件:`useUserStore().getUserInfo.userId === 73`。`getUserInfo` 是 `userStore.info` 的 computed,类型 `Partial`,`userId` 可能为 `undefined`,`undefined === 73` 为 `false`,判断安全,无需额外判空。 ### 3.2 前端逻辑 - 新增本地状态 `viewScope = ref<'all' | 'self'>('self')`,初值 `'self'`(即默认「我的」)。 - 切换控件绑定 `viewScope`;`@change` 时: 1. `pagination.currentPage = 1`(切回第一页,避免越界空页); 2. 调用 `getList()`。 - `getList()` 构造参数时,当 `userId === 73` 把 `viewScope.value` 作为 `view_scope` 一并传入: - 「全部」→ `view_scope: 'all'` - 「我的」→ `view_scope: 'self'` - 非 73 用户:不渲染控件,`viewScope` 恒为 `'self'`,传 `view_scope: 'self'`(或不传,见 3.3)。为简单起见统一传,后端对非特权用户会忽略该值。 ### 3.3 请求参数 列表请求参数由 ```jsonc { "current": 1, "size": 10, "is_final": 0, "keyword": "" } ``` 扩展为(73 用户) ```jsonc { "current": 1, "size": 10, "is_final": 0, "keyword": "", "view_scope": "all" } ``` - 参数名 `view_scope` 沿用现有 snake_case 风格(与 `is_final` / `keyword` 一致)。 - 取值为语义化的 `'all' | 'self'`,**不传用户 id**。 ### 3.4 后端配合 `GET /crm/stock/order/list` 增加可选参数 `view_scope`,处理逻辑: | 请求情况 | 后端行为 | |---|---| | 不传 / `view_scope=self` | 现状逻辑:按 token 用户过滤,返回自己的数据 | | `view_scope=all` 且 token 用户 id ∈ {73} | 返回全公司全量数据 | | `view_scope=all` 且 token 用户 id ∉ {73} | 降级:当作 `self` 处理,返回该用户自己的数据(忽略 `all`) | 后端「可看全部」白名单暂写死为 `{73}`,与前端判断一致。 ### 3.5 默认值 73 用户打开页面,`viewScope` 初值 `'self'`,即默认「我的」。需看全部时手动切换。 ## 4. 安全考量 - **后端只认 token**:是否返回全量数据,由后端依据 token 中的用户 id 判断,绝不信任前端传入的任何用户标识。前端 `userId` 存于 localStorage,可被篡改。 - 前端写死 `userId === 73` 仅用于**控件显隐**的便捷实现,不是安全边界。即便有人改前端强制显示按钮并传 `view_scope=all`,后端鉴权不通过仍只返回其自己的数据。 - 结论:语义化参数 + 后端 token 鉴权,可兜住水平越权(IDOR)风险。 ## 5. 改动清单 | 端 | 文件 / 接口 | 改动 | |---|---|---| | 前端 | `stocking_list.vue` | 工具栏加「全部/我的」`el-radio-group`(仅 73 显示);`getList` 传 `view_scope`;切换重置页码并重新请求 | | 前端 | `final_list.vue` | 同上(`is_final=1`) | | 后端 | `GET /crm/stock/order/list` | 接收 `view_scope`;token 用户 ∈ {73} 且 `=all` 时返回全量,否则按现状过滤 | ## 6. 验收标准 1. 以非 73 账号登录两个列表页:不出现「全部/我的」控件,数据范围与改动前一致。 2. 以 73 账号登录: - 默认显示「我的」选中态,列表为 73 自己的数据。 - 切到「全部」,列表变为全公司数据;切回「我的」恢复。 - 切换时分页重置到第 1 页。 3. 用任意账号在浏览器手动把请求改成 `view_scope=all`:后端只返回该账号自己的数据(非 73 不越权)。 ## 7. 未做项 / 后续 TODO - 前端 `userId === 73` 为硬编码,加人需改前端发版。后续建议:`getUserInfo` 下发权限标记(如 `stock@view_all` / `final_stock@view_all`),前端用现有 `hasAuth(...)` 控制显隐;后端白名单同步改为权限点判断。本次保留此扩展点,先不动。 - `view_scope` 选择不持久化(每次进页面回到默认「我的」)。