feat(supply-chain): 各部门数据分类去掉「数据权限例外」,权限统一以用户管理后台为准
- 配置弹窗移除「数据权限例外」输入项,保存时不再写入该字段(旧配置下次保存即清空) - 数据范围不再按人收窄:部门负责人/总负责人/普通账号一律看全部利润中心 - 设置结构、settings 接口返回与权限节点描述同步移除 dataScopeGrants
This commit is contained in:
parent
1779d8a697
commit
1a53aa31a3
@ -28,13 +28,12 @@ type SettingsDraft = {
|
||||
virtualOwnerProfitCenter: string
|
||||
ownerProfitCenterOverride: string
|
||||
profitCenterAlias: string
|
||||
dataScopeGrants: string
|
||||
targetRows: TargetRowDraft[]
|
||||
}
|
||||
|
||||
const EMPTY_DRAFT: SettingsDraft = {
|
||||
brandScope: '', brandOwnerAlias: '', virtualOwnerProfitCenter: '', ownerProfitCenterOverride: '',
|
||||
profitCenterAlias: '', dataScopeGrants: '',
|
||||
profitCenterAlias: '',
|
||||
targetRows: [{ profitCenter: '', newProductSuccess: '', newProductMoving: '', saRatio: '', saTargetRatio: '' }],
|
||||
}
|
||||
|
||||
@ -81,18 +80,6 @@ function mapToLines(value: Record<string, string>) {
|
||||
return Object.entries(value).map(([key, item]) => `${key}=${item}`).join('\n')
|
||||
}
|
||||
|
||||
function grantsToLines(value: Array<{ user: string; scopes: string[] }>) {
|
||||
return value.map((grant) => `${grant.user}=${grant.scopes.join(',')}`).join('\n')
|
||||
}
|
||||
|
||||
function linesToGrants(value: string) {
|
||||
return value.split('\n').map((line) => line.trim()).filter(Boolean).map((line) => {
|
||||
const separator = line.includes('=') ? '=' : line.includes(':') ? ':' : ':'
|
||||
const [user, ...rest] = line.split(separator)
|
||||
return { user: user.trim(), scopes: rest.join(separator).split(/[,,;;]+/).map((item) => item.trim()).filter(Boolean) }
|
||||
}).filter((grant) => grant.user && grant.scopes.length > 0)
|
||||
}
|
||||
|
||||
function targetRowPayload(row: TargetRowDraft | undefined) {
|
||||
return {
|
||||
newProductSuccess: Number(row?.newProductSuccess ?? 0),
|
||||
@ -245,7 +232,6 @@ export function DepartmentDistributionView({
|
||||
virtualOwnerProfitCenter: mapToLines(settings.virtualOwnerProfitCenter ?? {}),
|
||||
ownerProfitCenterOverride: mapToLines(settings.ownerProfitCenterOverride ?? {}),
|
||||
profitCenterAlias: mapToLines(settings.profitCenterAlias ?? {}),
|
||||
dataScopeGrants: grantsToLines(settings.dataScopeGrants ?? []),
|
||||
targetRows: [
|
||||
{
|
||||
profitCenter: '',
|
||||
@ -277,7 +263,6 @@ export function DepartmentDistributionView({
|
||||
virtualOwnerProfitCenter: linesToMap(draft.virtualOwnerProfitCenter),
|
||||
ownerProfitCenterOverride: linesToMap(draft.ownerProfitCenterOverride),
|
||||
profitCenterAlias: linesToMap(draft.profitCenterAlias),
|
||||
dataScopeGrants: linesToGrants(draft.dataScopeGrants),
|
||||
targets: targetRowPayload(draft.targetRows.find((row) => !row.profitCenter) ?? draft.targetRows[0]),
|
||||
profitCenterTargets: draft.targetRows
|
||||
.filter((row) => row.profitCenter)
|
||||
@ -405,10 +390,6 @@ export function DepartmentDistributionView({
|
||||
利润中心合并(一行一条:原利润中心=并入的利润中心,如 运营中心E=运营中心G)
|
||||
<textarea value={draft.profitCenterAlias} onChange={(event) => setDraft((current) => ({ ...current, profitCenterAlias: event.target.value }))} />
|
||||
</label>
|
||||
<label className={styles.deptConfigField}>
|
||||
数据权限例外(一行一条:花名=范围;范围支持 全部(看所有利润中心)/ 利润中心:xx / 事业部:xx / 品类部:xx / 负责人:xx,多个用逗号)
|
||||
<textarea value={draft.dataScopeGrants} onChange={(event) => setDraft((current) => ({ ...current, dataScopeGrants: event.target.value }))} />
|
||||
</label>
|
||||
<div className={styles.deptConfigField} style={{ gridColumn: '1 / -1' }}>
|
||||
目标参数(一行一个利润中心,可分别设置;利润中心选「默认」= 其它中心统一用这行目标)
|
||||
<table className={modalStyles.targetTable}>
|
||||
|
||||
@ -44,7 +44,7 @@ export type SupplyChainPermissionTargetKey =
|
||||
| 'productSabc'
|
||||
// 模块权限:商品SABC打标「各部门数据分类」(可见/下载分开控制)
|
||||
| 'departmentDistribution'
|
||||
// 商品SABC打标「各部门数据分类」的配置(品牌范围/兜底规则/数据权限例外/目标参数)
|
||||
// 商品SABC打标「各部门数据分类」的配置(品牌范围/兜底规则/目标参数)
|
||||
| 'departmentDistributionConfig'
|
||||
|
||||
export type SupplyChainPermissionSubject = Omit<
|
||||
@ -107,7 +107,7 @@ export const SUPPLY_CHAIN_PERMISSION_TARGETS: SupplyChainPermissionTarget[] = [
|
||||
{ key: 'productSabcCalculation', label: '商品SABC打标 · 执行核算', description: '每月核算与重新生成商品SABC打标数据(页面内按钮)' },
|
||||
{ key: 'departmentDistribution', label: '商品SABC打标 · 各部门数据分类', description: '按利润中心汇总的部门数据(可见、下载分别控制)' },
|
||||
{ key: 'productSabc', label: '商品SABC打标 · 商品明细', description: '商品SABC打标明细页与明细导出(默认全员开放,可在用户管理后台关闭)' },
|
||||
{ key: 'departmentDistributionConfig', label: '商品SABC打标 · 部门数据配置', description: '维护品牌范围、负责人兜底、数据权限例外与目标参数(操作列控制)' },
|
||||
{ key: 'departmentDistributionConfig', label: '商品SABC打标 · 部门数据配置', description: '维护品牌范围、负责人兜底与目标参数(操作列控制)' },
|
||||
]
|
||||
|
||||
/**
|
||||
|
||||
@ -100,11 +100,6 @@ export function clearDepartmentDistributionCache() {
|
||||
departmentRawCache.clear()
|
||||
}
|
||||
|
||||
export type DepartmentDataScopeGrant = {
|
||||
user: string
|
||||
scopes: string[]
|
||||
}
|
||||
|
||||
export type DepartmentDistributionSettings = {
|
||||
brandScope: string[]
|
||||
brandOwnerAlias: Record<string, string>
|
||||
@ -113,7 +108,6 @@ export type DepartmentDistributionSettings = {
|
||||
profitCenterAlias: Record<string, string>
|
||||
targets: DepartmentTargets
|
||||
profitCenterTargets: DepartmentProfitCenterTargets[]
|
||||
dataScopeGrants: DepartmentDataScopeGrant[]
|
||||
}
|
||||
|
||||
type OwnerConfigRow = {
|
||||
@ -171,7 +165,6 @@ export function normalizeDepartmentSettings(value: unknown): DepartmentDistribut
|
||||
}
|
||||
})
|
||||
.filter((item) => item.profitCenter)
|
||||
const grants = Array.isArray(raw.dataScopeGrants) ? raw.dataScopeGrants : []
|
||||
return {
|
||||
brandScope: cleanStringArray(raw.brandScope, DEFAULT_BRAND_SCOPE),
|
||||
brandOwnerAlias: cleanStringMap(raw.brandOwnerAlias, DEFAULT_BRAND_OWNER_ALIAS),
|
||||
@ -180,14 +173,6 @@ export function normalizeDepartmentSettings(value: unknown): DepartmentDistribut
|
||||
profitCenterAlias: cleanStringMap(raw.profitCenterAlias, DEFAULT_PROFIT_CENTER_ALIAS),
|
||||
targets: defaultTargets,
|
||||
profitCenterTargets,
|
||||
dataScopeGrants: grants
|
||||
.map((grant) => {
|
||||
const item = asRecord(grant)
|
||||
const user = String(item.user ?? '').trim()
|
||||
const scopes = cleanStringArray(item.scopes, [])
|
||||
return { user, scopes }
|
||||
})
|
||||
.filter((grant) => grant.user && grant.scopes.length > 0),
|
||||
}
|
||||
}
|
||||
|
||||
@ -385,55 +370,21 @@ export type DepartmentViewerScope = {
|
||||
}
|
||||
|
||||
/**
|
||||
* 方案 C 数据权限:
|
||||
* 1) 管理员看全部;
|
||||
* 2) 部门负责人(人员信息「部门负责人」)看本部门全部数据;
|
||||
* 3) 事业部/总负责人(「总负责人」)看本事业部全部数据;
|
||||
* 4) 配置里的例外授权可按 利润中心 / 事业部 / 品类部 / 负责人 追加范围;
|
||||
* 5) 其余人员只看自己名下的数据。
|
||||
* 「各部门数据分类」数据范围:不做任何按人收窄,所有人都能看到全部利润中心数据。
|
||||
* 查看/导出权限统一以「用户管理后台」的「商品SABC打标 · 各部门数据分类」节点为准(默认开放)。
|
||||
*/
|
||||
export async function resolveDepartmentViewerScope(
|
||||
user: AuthUser,
|
||||
configs: OwnerConfigRow[],
|
||||
settings: DepartmentDistributionSettings,
|
||||
): Promise<DepartmentViewerScope> {
|
||||
if (isAdmin(user)) return { nickname: '', isAdmin: true, allowedOwners: null, scopeLabel: '全部利润中心(管理员)' }
|
||||
const nickname = await resolveNickname(user, configs)
|
||||
const allowed = new Set<string>(nickname ? [nickname] : [])
|
||||
const scopes = new Set<string>()
|
||||
for (const config of configs) {
|
||||
if (!config.operationOwner) continue
|
||||
if (nickname && config.departmentOwner === nickname && config.department) scopes.add(`事业部:${config.department}`)
|
||||
if (nickname && config.generalOwner === nickname && config.department) scopes.add(`事业部:${config.department}`)
|
||||
return {
|
||||
nickname,
|
||||
isAdmin: false,
|
||||
allowedOwners: null,
|
||||
scopeLabel: nickname ? `全部利润中心(${nickname})` : '全部利润中心',
|
||||
}
|
||||
for (const grant of settings.dataScopeGrants) {
|
||||
if (grant.user !== nickname) continue
|
||||
for (const scope of grant.scopes) scopes.add(scope)
|
||||
}
|
||||
// 支持「全部」授权:scope 写 全部 / 所有 / * 即可看全部利润中心(不需要逐条列利润中心)
|
||||
const grantsAllData = [...scopes].some((scope) => {
|
||||
const normalized = scope.replace(/\s/g, '')
|
||||
return normalized === '全部' || normalized === '所有' || normalized === '*' || normalized === '全公司'
|
||||
})
|
||||
if (grantsAllData) {
|
||||
return { nickname, isAdmin: false, allowedOwners: null, scopeLabel: nickname ? `全部数据(授权给 ${nickname})` : '全部数据' }
|
||||
}
|
||||
for (const scope of scopes) {
|
||||
const [kind, rawValue] = scope.split(':')
|
||||
const value = String(rawValue ?? '').trim()
|
||||
if (!kind || !value) continue
|
||||
for (const config of configs) {
|
||||
if (!config.operationOwner) continue
|
||||
if (kind === '负责人' && config.operationOwner === value) allowed.add(config.operationOwner)
|
||||
if (kind === '事业部' && config.department === value) allowed.add(config.operationOwner)
|
||||
if (kind === '品类部' && config.secondaryDepartment === value) allowed.add(config.operationOwner)
|
||||
if (kind === '利润中心' && config.profitCenter === value) allowed.add(config.operationOwner)
|
||||
}
|
||||
}
|
||||
const scopeLabel = scopes.size > 0
|
||||
? [nickname ? `本人(${nickname})` : '', ...scopes].filter(Boolean).join('、')
|
||||
: nickname ? `本人(${nickname})` : '未匹配到人员信息(无数据)'
|
||||
return { nickname, isAdmin: false, allowedOwners: allowed, scopeLabel }
|
||||
}
|
||||
|
||||
function resolveProfitCenter(
|
||||
@ -812,9 +763,8 @@ export class SupplyChainDepartmentDistributionService {
|
||||
const cached = departmentResultCache.get(cacheKey)
|
||||
if (cached && Date.now() - cached.at < DEPARTMENT_RESULT_CACHE_TTL_MS) return cached.value
|
||||
|
||||
const settings = await loadDepartmentSettings()
|
||||
const configs = await loadOwnerConfigs()
|
||||
const viewer = await resolveDepartmentViewerScope(input.user, configs, settings)
|
||||
const viewer = await resolveDepartmentViewerScope(input.user, configs)
|
||||
const snapshot = await this.loadRawSnapshot(input.year, input.month)
|
||||
let result: DepartmentDistributionResult
|
||||
if (snapshot) {
|
||||
|
||||
@ -82,54 +82,40 @@ describe('supply-chain department distribution', () => {
|
||||
expect(settings.targets.newProductSuccess).toBe(0.3)
|
||||
})
|
||||
|
||||
it('keeps admin, department owner, general owner and grant scopes apart', async () => {
|
||||
it('gives everyone all profit centers (权限只看用户管理后台)', async () => {
|
||||
const configs = [
|
||||
config({ operationOwner: '比干', profitCenter: '运营中心A', department: '事业一部', departmentOwner: '比干', generalOwner: '比干' }),
|
||||
config({ operationOwner: '西门吹雪', profitCenter: '运营中心A', department: '事业二部', secondaryDepartment: '品类三部', departmentOwner: '西门吹雪', generalOwner: '無尘' }),
|
||||
config({ operationOwner: '玄女', profitCenter: '运营中心A', department: '事业二部', secondaryDepartment: '品类二部', departmentOwner: '西门吹雪', generalOwner: '無尘' }),
|
||||
config({ operationOwner: '香茗', profitCenter: '运营中心B', department: '运营一部' }),
|
||||
]
|
||||
const settings = normalizeDepartmentSettings(null)
|
||||
|
||||
const admin = await resolveDepartmentViewerScope(
|
||||
{ id: 'u', email: '', name: '管理员', role: 'ADMIN', status: 'ACTIVE' },
|
||||
configs,
|
||||
settings,
|
||||
)
|
||||
expect(admin.allowedOwners).toBeNull()
|
||||
expect(admin.scopeLabel).toContain('管理员')
|
||||
|
||||
// 部门负责人:比干 管 事业一部
|
||||
// 部门负责人 / 总负责人 / 普通人员都不再收窄,全部可见
|
||||
const departmentOwner = await resolveDepartmentViewerScope(
|
||||
{ id: 'u', email: '', name: '比干', role: 'USER', status: 'ACTIVE' },
|
||||
configs,
|
||||
settings,
|
||||
)
|
||||
expect([...(departmentOwner.allowedOwners ?? [])].sort()).toEqual(['比干'])
|
||||
expect(departmentOwner.allowedOwners).toBeNull()
|
||||
expect(departmentOwner.scopeLabel).toContain('全部利润中心')
|
||||
|
||||
// 总负责人:無尘 管 事业二部 → 西门吹雪 + 玄女
|
||||
const generalOwner = await resolveDepartmentViewerScope(
|
||||
{ id: 'u', email: '', name: '無尘', role: 'USER', status: 'ACTIVE' },
|
||||
configs,
|
||||
settings,
|
||||
)
|
||||
expect([...(generalOwner.allowedOwners ?? [])].sort()).toEqual(['無尘', '玄女', '西门吹雪'])
|
||||
expect(generalOwner.allowedOwners).toBeNull()
|
||||
|
||||
// 例外授权:香茗 额外看 利润中心:运营中心A
|
||||
const granted = await resolveDepartmentViewerScope(
|
||||
const ordinary = await resolveDepartmentViewerScope(
|
||||
{ id: 'u', email: '', name: '香茗', role: 'USER', status: 'ACTIVE' },
|
||||
configs,
|
||||
normalizeDepartmentSettings({ dataScopeGrants: [{ user: '香茗', scopes: ['利润中心:运营中心A'] }] }),
|
||||
)
|
||||
expect([...(granted.allowedOwners ?? [])].sort()).toEqual(['香茗', '比干', '玄女', '西门吹雪'].sort())
|
||||
|
||||
// 「全部」授权:不限制利润中心
|
||||
const full = await resolveDepartmentViewerScope(
|
||||
{ id: 'u', email: '', name: '望舒', role: 'USER', status: 'ACTIVE' },
|
||||
configs,
|
||||
normalizeDepartmentSettings({ dataScopeGrants: [{ user: '望舒', scopes: ['全部'] }] }),
|
||||
)
|
||||
expect(full.allowedOwners).toBeNull()
|
||||
expect(full.scopeLabel).toContain('全部数据')
|
||||
expect(ordinary.allowedOwners).toBeNull()
|
||||
})
|
||||
|
||||
it('aggregates rows by profit center, department and owner with the manual-table metrics', async () => {
|
||||
@ -295,15 +281,15 @@ describe('supply-chain department distribution', () => {
|
||||
expect(adminResult.profitCenters.map((center) => center.profitCenter)).toEqual(['运营中心A', '运营中心B'])
|
||||
expect(adminResult.designerTotal.productTotal).toBe(10)
|
||||
|
||||
// 普通人员只能看到自己那部分,小计按可见数据重算
|
||||
// 默认不做任何配置:普通人员也能看到全部利润中心(查看/导出由用户管理后台控制)
|
||||
clearDepartmentDistributionCache()
|
||||
const selfResult = await supplyChainDepartmentDistributionService.build({
|
||||
year: 2026, month: 8,
|
||||
user: { id: 'u', email: '', name: '比干', role: 'USER', status: 'ACTIVE' },
|
||||
canExport: false,
|
||||
})
|
||||
expect(selfResult.summary.productTotal).toBe(10)
|
||||
expect(selfResult.profitCenters.map((center) => center.profitCenter)).toEqual(['运营中心A'])
|
||||
expect(selfResult.summary.productTotal).toBe(20)
|
||||
expect(selfResult.profitCenters.map((center) => center.profitCenter)).toEqual(['运营中心A', '运营中心B'])
|
||||
expect(selfResult.designerTotal.productTotal).toBe(10)
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user