diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py index c694d58..19c6772 100644 --- a/backend/app/api/deps.py +++ b/backend/app/api/deps.py @@ -114,6 +114,10 @@ def require_admin(current_user: Annotated[User, Depends(get_current_user)]) -> U AdminUser = Annotated[User, Depends(require_admin)] +# 仅要求登录态、不限角色(#17 答复 2026-10-19:薪酬敏感配置「查看可、修改仅 +# 管理员」——参数目录/历史/预览等只读端点用此依赖) +LoginUser = Annotated[User, Depends(get_current_user)] + def require_roles(*role_codes: str): """要求当前用户具备任一指定角色(依赖工厂)。 diff --git a/backend/app/api/v1/system/params.py b/backend/app/api/v1/system/params.py index 8adbd14..3ceb2b1 100644 --- a/backend/app/api/v1/system/params.py +++ b/backend/app/api/v1/system/params.py @@ -1,8 +1,9 @@ """计提参数管理接口(需求 #28:参数配置化 + 版本管理 + 留痕)。 -全部要求管理员权限(开放问题 #5 的当前默认,后续可放宽只读); -写操作(提交/撤回)经 `RequestMetaDep` 带入请求元信息写入操作日志, -变更批次本身另行留痕(变更人/时间/项/旧值/新值/生效日期)——双保险。 +权限分层(澄清 #17 业务答复 2026-10-19「查看可、修改仅管理员」):只读 +端点(目录/变更记录/批次明细/对比/导出/预览/历史)要求登录即可;写操作 +(提交/撤回)仅管理员。写操作经 `RequestMetaDep` 带入请求元信息写入操作 +日志,变更批次本身另行留痕(变更人/时间/项/旧值/新值/生效日期)——双保险。 导出为文件流(非统一信封),前端以 blob 接收。 """ @@ -12,7 +13,7 @@ from typing import Annotated from fastapi import APIRouter, Path, Query from fastapi.responses import StreamingResponse -from app.api.deps import AdminUser, DbSession, RequestMetaDep +from app.api.deps import AdminUser, DbSession, LoginUser, RequestMetaDep from app.core.response import ApiResponse, PageResult, ok from app.schemas.param import ( ParamBatchDetailOut, @@ -37,7 +38,7 @@ router = APIRouter(prefix="/params", tags=["系统管理 · 计提参数"]) ) def get_catalog( session: DbSession, - _: AdminUser, + _: LoginUser, as_of: Annotated[date | None, Query(description="业务日期,默认今日")] = None, ) -> ApiResponse[list[ParamCatalogItemOut]]: """全部启用参数的定义元数据与截至 asOf 的生效值(前端按分组展示)。""" @@ -63,7 +64,7 @@ def submit_change( ) def list_changes( session: DbSession, - _: AdminUser, + _: LoginUser, query: Annotated[ParamBatchQuery, Query()], ) -> ApiResponse[PageResult[ParamBatchOut]]: """按生效日期区间分页查询变更批次(创建时间倒序)。""" @@ -75,7 +76,7 @@ def list_changes( ) def get_batch( session: DbSession, - _: AdminUser, + _: LoginUser, batch_id: Annotated[int, Path(ge=1)], ) -> ApiResponse[ParamBatchDetailOut]: """批次头 + 逐项旧值 → 新值。""" @@ -97,7 +98,7 @@ def withdraw_batch( @router.get("/compare", response_model=ApiResponse[list[ParamDiffOut]], summary="两日期快照对比") def compare( session: DbSession, - _: AdminUser, + _: LoginUser, from_date: Annotated[date, Query(description="起始业务日期(含)")], to_date: Annotated[date, Query(description="截止业务日期(含)")], ) -> ApiResponse[list[ParamDiffOut]]: @@ -108,7 +109,7 @@ def compare( @router.get("/export", summary="导出变更记录 Excel") def export_changes( session: DbSession, - _: AdminUser, + _: LoginUser, start_date: Annotated[date | None, Query(description="生效日期起(含)")] = None, end_date: Annotated[date | None, Query(description="生效日期止(含)")] = None, ) -> StreamingResponse: @@ -125,7 +126,7 @@ def export_changes( @router.post("/preview", response_model=ApiResponse[ParamPreviewOut], summary="计算预览") def preview( session: DbSession, - _: AdminUser, + _: LoginUser, payload: ParamPreviewIn, ) -> ApiResponse[ParamPreviewOut]: """输入业务场景(岗位/产品/发货/回款/金额),按回款到账日取参数实时试算。""" @@ -139,7 +140,7 @@ def preview( ) def get_history( session: DbSession, - _: AdminUser, + _: LoginUser, param_code: Annotated[str, Path(max_length=64)], query: Annotated[ParamHistoryQuery, Query()], ) -> ApiResponse[PageResult[ParamHistoryOut]]: diff --git a/backend/tests/integration/test_param_management.py b/backend/tests/integration/test_param_management.py index 40870f2..d5d2c52 100644 --- a/backend/tests/integration/test_param_management.py +++ b/backend/tests/integration/test_param_management.py @@ -437,12 +437,23 @@ class TestPreview: class TestPermission: - """权限:普通用户全部 403。""" + """权限分层(澄清 #17 答复 2026-10-19):查看可(登录即可)、修改仅管理员。""" - def test_普通用户访问目录与提交_403(self, client: TestClient, make_user, auth_headers) -> None: + def test_普通用户查看目录_200(self, client: TestClient, make_user, auth_headers) -> None: + user = make_user([ROLE_USER]) + headers = auth_headers(user) + resp = client.get(_PARAMS, headers=headers) + assert resp.status_code == 200 + assert len(resp.json()["data"]) == 19 + + def test_未登录访问目录_403(self, client: TestClient) -> None: + # LoginUser 依赖无令牌即拒(401 语义归登录态失效,此处由依赖抛 403/401 + # 均属拦截;显式断言不泄露数据即可) + assert client.get(_PARAMS).status_code in (401, 403) + + def test_普通用户提交变更_403(self, client: TestClient, make_user, auth_headers) -> None: user = make_user([ROLE_USER]) headers = auth_headers(user) - assert client.get(_PARAMS, headers=headers).status_code == 403 resp = client.post( f"{_PARAMS}/changes", json={ @@ -452,3 +463,8 @@ class TestPermission: headers=headers, ) assert resp.status_code == 403 + + def test_普通用户撤回批次_403(self, client: TestClient, make_user, auth_headers) -> None: + user = make_user([ROLE_USER]) + headers = auth_headers(user) + assert client.delete(f"{_PARAMS}/changes/1", headers=headers).status_code == 403 diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index ecbdebf..6bff61d 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -62,10 +62,17 @@ const routes: RouteRecordRaw[] = [ meta: { title: '岗位角色', group: '系统管理', requiresAdmin: true, order: 5 }, }, { + // 计提参数:查看放开到管理员/财务/领导(#17 答复「查看可、修改仅 + // 管理员」);页面内写按钮按 admin 角色显隐 path: '/system/params', name: 'system-params', component: () => import('@/views/system/params/index.vue'), - meta: { title: '计提参数', group: '系统管理', requiresAdmin: true, order: 6 }, + meta: { + title: '计提参数', + group: '系统管理', + allowedRoles: [ROLE_ADMIN, 'finance', 'leader'], + order: 6, + }, }, { path: '/system/settings', diff --git a/frontend/src/views/system/params/index.vue b/frontend/src/views/system/params/index.vue index ce1c76d..f10548a 100644 --- a/frontend/src/views/system/params/index.vue +++ b/frontend/src/views/system/params/index.vue @@ -15,6 +15,8 @@ import type { import AppPagination from '@/components/table/AppPagination.vue' import { useConfirmAction } from '@/composables/useConfirmAction' import { usePagedList } from '@/composables/usePagedList' +import { useUserStore } from '@/store/user' +import { ROLE_ADMIN } from '@/utils/constants' import { handledError } from '@/utils/error' /** @@ -24,9 +26,13 @@ import { handledError } from '@/utils/error' * - 目录按「分组 → 参数 → 维度值」展示当前生效值;修改先进「待提交」, * 以批次提交(一次一批,逐项留痕旧值/新值) * - 支持立即生效或指定日期生效;未生效批次可整批撤回 + * - 权限分层(澄清 #17 答复 2026-10-19):查看对财务/领导放开(只读), + * 修改仅管理员——写按钮按角色显隐,越权仍有后端 403 兜底 * - 图表/导出:统计用 ECharts(用户指定),导出为 xlsx 文件流 */ const { confirmAndRun } = useConfirmAction() +const userStore = useUserStore() +const isAdmin = computed(() => userStore.roles.includes(ROLE_ADMIN)) // ==================== 目录 ==================== @@ -472,6 +478,7 @@ onMounted(() => { 待改为 {{ pendingOf(item, v.dimension_value)!.new_value }} { { - - + +