feat/m3-increments #1
@ -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):
|
||||
"""要求当前用户具备任一指定角色(依赖工厂)。
|
||||
|
||||
@ -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]]:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 }}
|
||||
</el-tag>
|
||||
<el-button
|
||||
v-if="isAdmin"
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@ -487,7 +494,7 @@ onMounted(() => {
|
||||
</td>
|
||||
<td>
|
||||
<el-button
|
||||
v-if="!item.values.length"
|
||||
v-if="isAdmin && !item.values.length"
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@ -504,8 +511,8 @@ onMounted(() => {
|
||||
</el-collapse>
|
||||
</el-card>
|
||||
|
||||
<!-- 待提交批次 -->
|
||||
<el-card v-if="pendingList.length" shadow="never" class="pending-card">
|
||||
<!-- 待提交批次(仅管理员可见可操作) -->
|
||||
<el-card v-if="isAdmin && pendingList.length" shadow="never" class="pending-card">
|
||||
<template #header>
|
||||
<div class="pending-head">
|
||||
<b>待提交变更({{ pendingList.length }} 项)</b>
|
||||
@ -670,7 +677,7 @@ onMounted(() => {
|
||||
<el-table-column label="操作" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.effective_date > localToday()"
|
||||
v-if="isAdmin && row.effective_date > localToday()"
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user