docs: add plan-archive price permission implementation plan
Break the approved design into SQL, backend mask/API, and web-admin permission wiring tasks with verification steps. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
513d3b0fb3
commit
27c1cbba15
1 file changed
+566
@@ -0,0 +1,566 @@
|
||||
# 运输计划归档价格权限 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 在现有【运输计划归档】页按按钮权限控制「归档操作」与「单价/金额可见」,前端隐藏 + 后端写拦截/读脱敏,不新建独立菜单页。
|
||||
|
||||
**Architecture:** 复用 `pl:plan-archive:archive`,新增 `pl:plan-archive:price`。后端用 `get_user_menu_codes`(对齐 `force-follow-status`)校验写接口;用纯函数对 `PlanWithProducts` 脱敏产品归档价与计划运费价。前端用 `fetchGetMenuCodes` 控制按钮/列/查看弹窗/导出列。
|
||||
|
||||
**Tech Stack:** FastAPI · SQLAlchemy · `sys_menu`/`sys_role_menu` · Vue 3 · Element Plus · `fetchGetMenuCodes`
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-03-pl-plan-archive-price-permission-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File map
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `docs/sql/migrations/2026-09-03-pl-plan-archive-price-perm.sql` | 插入 `PL_PLAN_ARCHIVE_PRICE`;为已有归档页角色补授权 |
|
||||
| `docs/sql/shanxia2.0.sql` | 基线同步插入 `sys_menu` |
|
||||
| `backend/app/services/pl_plan_price_mask.py` | 权限判断 + `PlanWithProducts` 脱敏纯函数 |
|
||||
| `backend/tests/test_pl_plan_price_mask.py` | 脱敏单测 |
|
||||
| `backend/app/api/pl/plan.py` | archive 写校验;list/get/export 调用脱敏/去列 |
|
||||
| `web-admin/src/views/pl/plan-archive/index.vue` | 加载权限;控按钮/列/弹窗 props/导出 |
|
||||
| `web-admin/src/components/business/pl-plan-view-dialog/index.vue` | 透传 `showArchivePriceFields` |
|
||||
| `web-admin/src/components/business/pl-plan-view-tabs/index.vue` | 价字段受 `showArchivePriceFields`;运费区隐藏价 |
|
||||
| `web-admin/src/components/business/pl-plan-form-dialog/PlPlanFormProductTable.vue` | 归档 IO 价列受 `showArchivePriceFields` |
|
||||
| `web-admin/src/components/business/pl-plan-archive-brand-view-block/index.vue` | 查看态品牌表可隐藏单价金额列 |
|
||||
| `web-admin/src/components/business/pl-plan-transport-feedback-blocks/index.vue` | 若透传 showArchiveIoFields,同步价格开关 |
|
||||
|
||||
**已知副作用(按设计接受):** 无 `pl:plan-archive:price` 时,`list_plans`/`get_plan` 会清空 `freightAmount`/`totalAmount` 及产品归档价。运输计划主列表/查看若也调同一接口,运费金额会对无该权限用户不可见。运费结算等独立模块本期不改。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 菜单 SQL + 角色补授权
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/sql/migrations/2026-09-03-pl-plan-archive-price-perm.sql`
|
||||
- Modify: `docs/sql/shanxia2.0.sql`(在 `PL_PLAN_ARCHIVE_Q` 附近追加 `PL_PLAN_ARCHIVE_PRICE`)
|
||||
|
||||
- [ ] **Step 1: 写增量迁移**
|
||||
|
||||
```sql
|
||||
-- 运输计划归档:查看单价金额按钮权限
|
||||
-- 设计: docs/superpowers/specs/2026-09-03-pl-plan-archive-price-permission-design.md
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
INSERT INTO `sys_menu` (
|
||||
`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`,
|
||||
`is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`,
|
||||
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
|
||||
)
|
||||
SELECT
|
||||
'PL_PLAN_ARCHIVE_PRICE', '查看单价金额', 'PL_PLAN_ARCHIVE', 'F', '', '',
|
||||
'pl:plan-archive:price', 1, '1', '1', 'shanxia2.0', 4, NULL,
|
||||
'system', '', NOW(), NULL, NOW(), 0
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `sys_menu` WHERE `id` = 'PL_PLAN_ARCHIVE_PRICE' AND `del_flag` = 0
|
||||
);
|
||||
|
||||
-- 已拥有归档页(父菜单)的角色:补 price
|
||||
INSERT INTO `sys_role_menu` (
|
||||
`id`, `role_id`, `menu_id`, `tenant_no`, `order_num`, `remark`,
|
||||
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
|
||||
)
|
||||
SELECT
|
||||
REPLACE(UUID(), '-', ''),
|
||||
rm.role_id,
|
||||
'PL_PLAN_ARCHIVE_PRICE',
|
||||
rm.tenant_no,
|
||||
0,
|
||||
NULL,
|
||||
'system',
|
||||
'',
|
||||
NOW(),
|
||||
NULL,
|
||||
NOW(),
|
||||
0
|
||||
FROM `sys_role_menu` rm
|
||||
WHERE rm.menu_id = 'PL_PLAN_ARCHIVE'
|
||||
AND rm.del_flag = 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `sys_role_menu` x
|
||||
WHERE x.role_id = rm.role_id
|
||||
AND x.menu_id = 'PL_PLAN_ARCHIVE_PRICE'
|
||||
AND x.del_flag = 0
|
||||
);
|
||||
|
||||
-- 已拥有「归档」按钮的角色:补 price(防止仅有按钮无父菜单行时漏补)
|
||||
INSERT INTO `sys_role_menu` (
|
||||
`id`, `role_id`, `menu_id`, `tenant_no`, `order_num`, `remark`,
|
||||
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
|
||||
)
|
||||
SELECT
|
||||
REPLACE(UUID(), '-', ''),
|
||||
rm.role_id,
|
||||
'PL_PLAN_ARCHIVE_PRICE',
|
||||
rm.tenant_no,
|
||||
0,
|
||||
NULL,
|
||||
'system',
|
||||
'',
|
||||
NOW(),
|
||||
NULL,
|
||||
NOW(),
|
||||
0
|
||||
FROM `sys_role_menu` rm
|
||||
WHERE rm.menu_id = 'PL_PLAN_ARCHIVE_A'
|
||||
AND rm.del_flag = 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `sys_role_menu` x
|
||||
WHERE x.role_id = rm.role_id
|
||||
AND x.menu_id = 'PL_PLAN_ARCHIVE_PRICE'
|
||||
AND x.del_flag = 0
|
||||
);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 同步 `docs/sql/shanxia2.0.sql` 的 `sys_menu` INSERT**
|
||||
|
||||
在现有 `PL_PLAN_ARCHIVE_Q` 行后增加与上相同字段的 `PL_PLAN_ARCHIVE_PRICE` 插入(`order_num=4`,`perms=pl:plan-archive:price`)。
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/sql/migrations/2026-09-03-pl-plan-archive-price-perm.sql docs/sql/shanxia2.0.sql
|
||||
git commit -m "chore(sql): add pl:plan-archive:price menu and backfill roles"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 后端脱敏纯函数(TDD)
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/services/pl_plan_price_mask.py`
|
||||
- Create: `backend/tests/test_pl_plan_price_mask.py`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from app.schemas.pl.plan import (
|
||||
PlanLoadRead,
|
||||
PlanProductRead,
|
||||
PlanStopProductRead,
|
||||
PlanWithProducts,
|
||||
)
|
||||
from app.services.pl_plan_price_mask import mask_plan_with_products_prices
|
||||
|
||||
|
||||
def _minimal_plan(**kwargs):
|
||||
base = dict(
|
||||
id="p1",
|
||||
plan_no="P001",
|
||||
create_date=date(2026, 9, 1),
|
||||
total_amount=Decimal("100.00"),
|
||||
freight_amount=Decimal("50.00"),
|
||||
tenant_no="t1",
|
||||
create_time=datetime(2026, 9, 1),
|
||||
update_time=datetime(2026, 9, 1),
|
||||
products=[],
|
||||
deliveries=[],
|
||||
)
|
||||
base.update(kwargs)
|
||||
return PlanWithProducts.model_validate(base)
|
||||
|
||||
|
||||
def test_mask_clears_plan_and_product_prices():
|
||||
product = PlanProductRead.model_validate(
|
||||
{
|
||||
"id": "pp1",
|
||||
"planId": "p1",
|
||||
"tenantNo": "t1",
|
||||
"createTime": "2026-09-01T00:00:00",
|
||||
"updateTime": "2026-09-01T00:00:00",
|
||||
"productId": "g1",
|
||||
"productNo": "N1",
|
||||
"productName": "尿素",
|
||||
"inboundPrice": "12.5",
|
||||
"inboundAmount": "125",
|
||||
"outboundPrice": "13",
|
||||
"outboundAmount": "130",
|
||||
"brandQtys": [
|
||||
{"brandId": "b1", "brandName": "君正", "qty": "10", "unitPrice": "12.5"}
|
||||
],
|
||||
}
|
||||
)
|
||||
stop_product = PlanStopProductRead.model_validate(
|
||||
{
|
||||
"id": "sp1",
|
||||
"planId": "p1",
|
||||
"tenantNo": "t1",
|
||||
"createTime": "2026-09-01T00:00:00",
|
||||
"updateTime": "2026-09-01T00:00:00",
|
||||
"productId": "g1",
|
||||
"productNo": "N1",
|
||||
"productName": "尿素",
|
||||
"inboundPrice": "12.5",
|
||||
"inboundAmount": "125",
|
||||
"brandQtys": [{"brandId": "b1", "qty": "10", "unitPrice": "12.5"}],
|
||||
}
|
||||
)
|
||||
plan = _minimal_plan(
|
||||
products=[product],
|
||||
load=PlanLoadRead.model_validate(
|
||||
{
|
||||
"id": "l1",
|
||||
"planId": "p1",
|
||||
"stopKind": "load",
|
||||
"products": [stop_product],
|
||||
"tenantNo": "t1",
|
||||
"createTime": "2026-09-01T00:00:00",
|
||||
"updateTime": "2026-09-01T00:00:00",
|
||||
}
|
||||
),
|
||||
)
|
||||
out = mask_plan_with_products_prices(plan)
|
||||
assert out.total_amount == Decimal("0")
|
||||
assert out.freight_amount == Decimal("0")
|
||||
assert out.products[0].inbound_price is None
|
||||
assert out.products[0].inbound_amount is None
|
||||
assert out.products[0].outbound_price is None
|
||||
assert out.products[0].outbound_amount is None
|
||||
assert out.products[0].brand_qtys[0].unit_price is None
|
||||
assert out.load.products[0].inbound_price is None
|
||||
assert out.load.products[0].brand_qtys[0].unit_price is None
|
||||
|
||||
|
||||
def test_mask_is_noop_when_disabled():
|
||||
plan = _minimal_plan(total_amount=Decimal("9"), freight_amount=Decimal("8"))
|
||||
out = mask_plan_with_products_prices(plan, enabled=False)
|
||||
assert out.total_amount == Decimal("9")
|
||||
assert out.freight_amount == Decimal("8")
|
||||
```
|
||||
|
||||
若 schema 必填字段与上不符,按 `backend/app/schemas/pl/plan.py` 补齐,保持断言语义。
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_pl_plan_price_mask.py -v
|
||||
```
|
||||
|
||||
Expected: FAIL(模块不存在)
|
||||
|
||||
- [ ] **Step 3: Implement `backend/app/services/pl_plan_price_mask.py`**
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.menu_guard import SUPER_ADMIN_ROLE, get_user_menu_codes
|
||||
from app.schemas.pl.plan import PlanWithProducts
|
||||
|
||||
ARCHIVE_PRICE_PERM = "pl:plan-archive:price"
|
||||
ARCHIVE_PERM = "pl:plan-archive:archive"
|
||||
|
||||
|
||||
def user_has_menu_perm(db: Session, user, perm: str) -> bool:
|
||||
if getattr(user, "role", None) == SUPER_ADMIN_ROLE:
|
||||
return True
|
||||
codes = get_user_menu_codes(db, user.id)
|
||||
return perm in codes
|
||||
|
||||
|
||||
def user_can_view_archive_prices(db: Session, user) -> bool:
|
||||
return user_has_menu_perm(db, user, ARCHIVE_PRICE_PERM)
|
||||
|
||||
|
||||
def user_can_archive_plan(db: Session, user) -> bool:
|
||||
return user_has_menu_perm(db, user, ARCHIVE_PERM)
|
||||
|
||||
|
||||
def _mask_brand_qtys(brand_qtys):
|
||||
if not brand_qtys:
|
||||
return brand_qtys
|
||||
return [bq.model_copy(update={"unit_price": None}) for bq in brand_qtys]
|
||||
|
||||
|
||||
def _mask_product_like(item):
|
||||
return item.model_copy(
|
||||
update={
|
||||
"inbound_price": None,
|
||||
"inbound_amount": None,
|
||||
"outbound_price": None,
|
||||
"outbound_amount": None,
|
||||
"brand_qtys": _mask_brand_qtys(getattr(item, "brand_qtys", None)),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def mask_plan_with_products_prices(
|
||||
plan: PlanWithProducts, *, enabled: bool = True
|
||||
) -> PlanWithProducts:
|
||||
if not enabled:
|
||||
return plan
|
||||
products = [_mask_product_like(p) for p in (plan.products or [])]
|
||||
load = plan.load
|
||||
if load is not None and getattr(load, "products", None):
|
||||
load = load.model_copy(
|
||||
update={"products": [_mask_product_like(p) for p in load.products]}
|
||||
)
|
||||
deliveries = []
|
||||
for d in plan.deliveries or []:
|
||||
if getattr(d, "products", None):
|
||||
deliveries.append(
|
||||
d.model_copy(
|
||||
update={"products": [_mask_product_like(p) for p in d.products]}
|
||||
)
|
||||
)
|
||||
else:
|
||||
deliveries.append(d)
|
||||
return plan.model_copy(
|
||||
update={
|
||||
"total_amount": Decimal("0"),
|
||||
"freight_amount": Decimal("0"),
|
||||
"products": products,
|
||||
"load": load,
|
||||
"deliveries": deliveries,
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
说明:`PlanRead.total_amount` / `freight_amount` 非 Optional,脱敏用 `Decimal("0")`。
|
||||
|
||||
- [ ] **Step 4: Run tests**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_pl_plan_price_mask.py -v
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/services/pl_plan_price_mask.py backend/tests/test_pl_plan_price_mask.py
|
||||
git commit -m "feat(backend): add plan archive price mask helpers"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 接线 plan API(写拦截 + 读脱敏 + 导出去列)
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/api/pl/plan.py`
|
||||
|
||||
- [ ] **Step 1: import 与导出字段常量**
|
||||
|
||||
```python
|
||||
from app.services.pl_plan_price_mask import (
|
||||
mask_plan_with_products_prices,
|
||||
user_can_archive_plan,
|
||||
user_can_view_archive_prices,
|
||||
)
|
||||
|
||||
ARCHIVE_PRICE_EXPORT_FIELDS = frozenset(
|
||||
{"unitPrice", "amount", "freightPrice", "freightAmount"}
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `list_plans` / `get_plan` 返回前**
|
||||
|
||||
```python
|
||||
can_price = user_can_view_archive_prices(db, user)
|
||||
plan_item = mask_plan_with_products_prices(plan_item, enabled=not can_price)
|
||||
# get_plan:
|
||||
result = mask_plan_with_products_prices(result, enabled=not can_price)
|
||||
```
|
||||
|
||||
`archive` / `archive-draft` 成功响应同样脱敏。
|
||||
|
||||
- [ ] **Step 3: `archive_plan` / `archive_plan_draft` 开头**
|
||||
|
||||
```python
|
||||
if not user_can_archive_plan(db, user):
|
||||
raise HTTPException(status_code=403, detail="无运输计划归档操作权限")
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 归档导出**
|
||||
|
||||
```python
|
||||
cols = filter_export_columns(PLAN_ARCHIVE_EXPORT_COLUMNS, body.columns)
|
||||
if not user_can_view_archive_prices(db, user):
|
||||
cols = [c for c in cols if c.field not in ARCHIVE_PRICE_EXPORT_FIELDS]
|
||||
```
|
||||
|
||||
确认 `ExportColumn` 使用 `.field`(与现有定义一致)。
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/api/pl/plan.py
|
||||
git commit -m "feat(backend): gate archive writes and mask plan prices by menu code"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 查看链路拆分数量与价格展示
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/components/business/pl-plan-view-dialog/index.vue`
|
||||
- Modify: `web-admin/src/components/business/pl-plan-view-tabs/index.vue`
|
||||
- Modify: `web-admin/src/components/business/pl-plan-form-dialog/PlPlanFormProductTable.vue`
|
||||
- Modify: `web-admin/src/components/business/pl-plan-archive-brand-view-block/index.vue`
|
||||
- Modify: `web-admin/src/components/business/pl-plan-transport-feedback-blocks/index.vue`(若向下传 IO 价)
|
||||
|
||||
- [ ] **Step 1: Dialog / Tabs 增加 prop**
|
||||
|
||||
```ts
|
||||
/** 归档查看:展示单价与金额;未传时跟随 showArchiveIoFields */
|
||||
showArchivePriceFields?: boolean
|
||||
```
|
||||
|
||||
```ts
|
||||
const effectiveShowArchivePrice = computed(
|
||||
() => props.showArchivePriceFields ?? props.showArchiveIoFields
|
||||
)
|
||||
```
|
||||
|
||||
Dialog 透传给 Tabs。
|
||||
|
||||
- [ ] **Step 2: 运费区**
|
||||
|
||||
仅当归档查看且无价权限时隐藏运费价字段:
|
||||
|
||||
```ts
|
||||
const hideArchiveMoney =
|
||||
props.showArchiveIoFields && !effectiveShowArchivePrice.value
|
||||
```
|
||||
|
||||
`freightViewFields` 在 `hideArchiveMoney` 时 omit「运输单价」「运费金额」两项(不展示占位 `—`)。
|
||||
|
||||
- [ ] **Step 3: 产品表**
|
||||
|
||||
`PlPlanFormProductTable`:新增 `showArchivePriceFields`(默认 `true`)。单价/金额列:
|
||||
|
||||
`v-if="showArchiveIoFields && showArchivePriceFields && ..."`
|
||||
|
||||
数量列仍跟 `showArchiveIoFields`。
|
||||
|
||||
段次表:`:show-stop-archive-price="effectiveShowArchivePrice"`。
|
||||
|
||||
- [ ] **Step 4: 品牌查看块**
|
||||
|
||||
增加 `showPrice?: boolean`(默认 `true`)。为 `false` 时不渲染单价、金额列。Tabs 传入 `:show-price="effectiveShowArchivePrice"`。
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add web-admin/src/components/business/pl-plan-view-dialog/index.vue \
|
||||
web-admin/src/components/business/pl-plan-view-tabs/index.vue \
|
||||
web-admin/src/components/business/pl-plan-form-dialog/PlPlanFormProductTable.vue \
|
||||
web-admin/src/components/business/pl-plan-archive-brand-view-block/index.vue \
|
||||
web-admin/src/components/business/pl-plan-transport-feedback-blocks/index.vue
|
||||
git commit -m "feat(web-admin): split archive qty vs price display flags"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 归档列表页接线权限
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/views/pl/plan-archive/index.vue`
|
||||
|
||||
- [ ] **Step 1: 加载权限**
|
||||
|
||||
```ts
|
||||
import { fetchGetMenuCodes } from '@/api/auth'
|
||||
|
||||
const canArchive = ref(false)
|
||||
const canViewPrice = ref(false)
|
||||
|
||||
async function loadArchivePermissions() {
|
||||
try {
|
||||
const perm = await fetchGetMenuCodes()
|
||||
const all = !!perm?.all || !!perm?.isSuperAdmin
|
||||
const codes = perm?.menuCodes || []
|
||||
canArchive.value = all || codes.includes('pl:plan-archive:archive')
|
||||
canViewPrice.value = all || codes.includes('pl:plan-archive:price')
|
||||
} catch {
|
||||
canArchive.value = false
|
||||
canViewPrice.value = false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`onMounted` 中先 `loadArchivePermissions()`。
|
||||
|
||||
- [ ] **Step 2: 按钮**
|
||||
|
||||
工具栏与查看弹窗 hero 的归档/调整按钮加 `v-if="canArchive"`。
|
||||
|
||||
- [ ] **Step 3: 列与统计**
|
||||
|
||||
无 `canViewPrice` 时 `listColumnDefs` 不含 `productUnitPrice`/`productAmount`;持久化 key 升为 `pl-plan-archive-list-columns-v6`;表格列与统计行双条件排除。
|
||||
|
||||
- [ ] **Step 4: ViewDialog**
|
||||
|
||||
```vue
|
||||
<PlPlanViewDialog
|
||||
show-archive-io-fields
|
||||
:show-archive-price-fields="canViewPrice"
|
||||
...
|
||||
/>
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 导出**
|
||||
|
||||
前端若传 `columns`,剔除价字段;**以后端 Task 3 过滤为准**。
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add web-admin/src/views/pl/plan-archive/index.vue
|
||||
git commit -m "feat(web-admin): gate archive actions and price columns by menu codes"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 验收与图谱
|
||||
|
||||
- [ ] **Step 1: 开发库执行** `docs/sql/migrations/2026-09-03-pl-plan-archive-price-perm.sql`
|
||||
|
||||
- [ ] **Step 2: 手工对照设计 §7 场景 A/C/D/E**
|
||||
|
||||
- [ ] **Step 3: 更新图谱**
|
||||
|
||||
```bash
|
||||
./scripts/update-graph.sh backend
|
||||
./scripts/update-graph.sh frontend
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit graphify(若有变更)**
|
||||
|
||||
```bash
|
||||
git add backend/graphify-out web-admin/graphify-out
|
||||
git commit -m "chore: update graphify after plan-archive price permission"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spec coverage checklist
|
||||
|
||||
| 设计要求 | Task |
|
||||
|----------|------|
|
||||
| 新建 `pl:plan-archive:price` + 角色补授权 | 1 |
|
||||
| 无 archive → 隐藏按钮 / 写 403 | 3, 5 |
|
||||
| 无 price → 列表/查看/品牌/运费价隐藏 | 4, 5 |
|
||||
| 无 price → 导出去列 | 3, 5 |
|
||||
| 读接口脱敏 | 2, 3 |
|
||||
| 不新建独立模块 | (全计划) |
|
||||
| ERP / edit 本期不改 | (未列入) |
|
||||
|
||||
## Self-review notes
|
||||
|
||||
- 无 TBD 占位步骤。
|
||||
- `mask_plan_with_products_prices` 与 API 接线命名一致。
|
||||
- `freight_amount` 脱敏用 `0` 而非 `null`。
|
||||
- 归档查看用 `showArchiveIoFields && !price` 限制运费价 UI 隐藏,避免误伤非归档页。
|
||||
Reference in new issue
Block a user