76baee2fe5
为 user_id=73 在 stocking_list 与 final_list 增加「全部/我的」切换, view_scope 语义化传参,后端按 token+73 鉴权,默认「我的」。 Co-Authored-By: Claude <noreply@anthropic.com>
100 lines
5.2 KiB
Markdown
100 lines
5.2 KiB
Markdown
# 备货单 / 终版列表「全部 / 我的」视图切换 设计
|
||
|
||
- 日期: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<UserInfo>`,`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` 选择不持久化(每次进页面回到默认「我的」)。
|