feat: 接入用户与文档权限管理

This commit is contained in:
xy
2026-09-03 18:43:42 +08:00
parent 6d56bc0075
commit f45864e817
24 changed files with 2139 additions and 775 deletions
@@ -6,9 +6,16 @@ import type {
BackendCollection,
BackendPage,
CreateTemplatePermissionRequest,
DepartmentPermissionPolicyRecord,
PermissionListResponse,
PermissionPolicyLevel,
PermissionPoliciesRecord,
PermissionPoliciesResponseDto,
PermissionPolicyDepartmentDto,
PermissionPolicyRoleDto,
PermissionQuery,
PermissionRecord,
RolePermissionPolicyRecord,
TemplatePermissionBatchRequest,
TemplatePermissionBatchResponse,
TemplatePermissionBatchResponseDto,
@@ -19,9 +26,12 @@ import type {
TemplatePermissionMatrixResponse,
TemplatePermissionRecord,
TemplatePermissionResponseDto,
UpdateDepartmentPermissionPolicyRequest,
UpdateRolePermissionPolicyRequest,
} from './types'
const useMockData = import.meta.env.VITE_USE_MOCK !== 'false'
// 仅显式开启 VITE_USE_MOCK=true 时使用演示数据,避免联调环境因未注入变量而误入 Mock 分支。
const useMockData = import.meta.env.VITE_USE_MOCK === 'true'
export const isPermissionMockEnabled = useMockData
@@ -44,6 +54,473 @@ const mockRecords: PermissionRecord[] = [
},
]
const mockPermissionPolicies: PermissionPoliciesRecord = {
campusId: 'mock-main',
roles: [
{
roleId: 'system-admin',
code: 'SYSTEM_ADMIN',
name: '系统管理员',
description: '系统配置、用户与权限管理、审计日志',
userCount: 2,
campusId: 'mock-main',
view: true,
use: true,
maintain: true,
permissionLevel: 'MAINTAIN',
},
{
roleId: 'medical-management',
code: 'MEDICAL_MANAGEMENT',
name: '医务管理',
description: '全院文档库维护、报表分析、任务监督',
userCount: 4,
campusId: 'mock-main',
view: true,
use: true,
maintain: true,
permissionLevel: 'MAINTAIN',
},
{
roleId: 'department-signer',
code: 'DEPARTMENT_SIGNER',
name: '科室签署员',
description: '本科室文档发起签署、手写板 / 短信操作',
userCount: 23,
campusId: 'mock-main',
view: true,
use: true,
maintain: false,
permissionLevel: 'USE',
},
{
roleId: 'audit-viewer',
code: 'AUDIT_VIEWER',
name: '查询审计员',
description: '只读查询报表与审计留痕,不可发起',
userCount: 3,
campusId: 'mock-main',
view: true,
use: false,
maintain: false,
permissionLevel: 'VIEW',
},
],
departments: [
{
departmentId: 'mock-department-radiology',
code: 'RADIOLOGY',
name: '放射科',
campusId: 'mock-main',
own: true,
common: true,
other: false,
ownPermission: 'USE',
commonPermission: 'USE',
otherPermission: 'NONE',
},
{
departmentId: 'mock-department-cardiology',
code: 'CARDIOLOGY',
name: '心内科',
campusId: 'mock-main',
own: true,
common: true,
other: false,
ownPermission: 'USE',
commonPermission: 'USE',
otherPermission: 'NONE',
},
{
departmentId: 'mock-department-orthopedics',
code: 'ORTHOPEDICS',
name: '骨科',
campusId: 'mock-main',
own: true,
common: true,
other: false,
ownPermission: 'USE',
commonPermission: 'USE',
otherPermission: 'NONE',
},
{
departmentId: 'mock-department-health',
code: 'HEALTH_MANAGEMENT',
name: '健康管理中心',
campusId: 'mock-main',
own: true,
common: true,
other: false,
ownPermission: 'USE',
commonPermission: 'USE',
otherPermission: 'NONE',
},
],
templateCount: 2,
}
type PermissionPolicyObject = Record<string, unknown>
function asObject(value: unknown): PermissionPolicyObject {
return value !== null && typeof value === 'object' && !Array.isArray(value)
? (value as PermissionPolicyObject)
: {}
}
function asArray(value: unknown): unknown[] {
return Array.isArray(value) ? value : []
}
function readString(source: PermissionPolicyObject, keys: string[], fallback = '') {
for (const key of keys) {
const value = source[key]
if (typeof value === 'string' && value.trim()) {
return value.trim()
}
}
return fallback
}
function readNumber(source: PermissionPolicyObject, keys: string[]) {
for (const key of keys) {
const value = source[key]
if (typeof value === 'number' && Number.isFinite(value)) {
return value
}
if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {
return Number(value)
}
}
return undefined
}
function readBoolean(source: PermissionPolicyObject, keys: string[]) {
for (const key of keys) {
const value = source[key]
if (typeof value === 'boolean') {
return value
}
if (typeof value === 'number') {
return value !== 0
}
if (typeof value === 'string') {
const normalized = value.trim().toLowerCase()
if (['true', '1', 'yes', 'allow', 'allowed', 'enabled', 'on'].includes(normalized)) {
return true
}
if (['false', '0', 'no', 'deny', 'denied', 'disabled', 'off'].includes(normalized)) {
return false
}
}
if (value !== null && typeof value === 'object') {
const nested = asObject(value)
const nestedValue = nested.enabled ?? nested.allowed ?? nested.value
if (typeof nestedValue === 'boolean') {
return nestedValue
}
}
}
return false
}
function readVersion(source: PermissionPolicyObject) {
return readNumber(source, ['version', 'policyVersion', 'revision'])
}
const policyLevelRank: Record<PermissionPolicyLevel, number> = {
NONE: 0,
VIEW: 1,
USE: 2,
MAINTAIN: 3,
}
function normalizePolicyLevel(value: unknown): PermissionPolicyLevel | undefined {
if (typeof value !== 'string') {
return undefined
}
const level = value.trim().toUpperCase() as PermissionPolicyLevel
return level in policyLevelRank ? level : undefined
}
function policyLevelFromFlags(source: PermissionPolicyObject, keys: string[]) {
let level: PermissionPolicyLevel = 'NONE'
keys.forEach((key, index) => {
if (readBoolean(source, [key])) {
const nextLevel = (['VIEW', 'USE', 'MAINTAIN'] as const)[index]
if (policyLevelRank[nextLevel] > policyLevelRank[level]) {
level = nextLevel
}
}
})
return level
}
function readPolicyLevel(
source: PermissionPolicyObject,
levelKeys: string[],
flagKeys: string[],
): PermissionPolicyLevel {
for (const key of levelKeys) {
const level = normalizePolicyLevel(source[key])
if (level) {
return level
}
}
return policyLevelFromFlags(source, flagKeys)
}
function flagsForPolicyLevel(level: PermissionPolicyLevel) {
const rank = policyLevelRank[level]
return {
view: rank >= policyLevelRank.VIEW,
use: rank >= policyLevelRank.USE,
maintain: rank >= policyLevelRank.MAINTAIN,
}
}
function departmentFlagsForPolicyLevel(level: PermissionPolicyLevel) {
return level !== 'NONE'
}
function mapRolePolicy(
dto: PermissionPolicyRoleDto | PermissionPolicyObject,
fallbackRoleId = '',
fallbackCampusId = '',
): RolePermissionPolicyRecord | null {
const source = asObject(dto)
const roleId = readString(source, ['roleId', 'roleID', 'id'], fallbackRoleId)
if (!roleId) {
return null
}
const permissionLevel = readPolicyLevel(
source,
['permissionLevel', 'maxPermissionLevel', 'level'],
['view', 'use', 'maintain'],
)
return {
roleId,
campusId: readString(source, ['campusId', 'campusID'], fallbackCampusId),
code: readString(source, ['code', 'roleCode'], roleId),
name: readString(source, ['name', 'roleName'], roleId),
description: readString(source, ['description'], '—'),
userCount: readNumber(source, ['userCount', 'users', 'memberCount']) ?? null,
...flagsForPolicyLevel(permissionLevel),
permissionLevel,
version: readVersion(source),
}
}
function mapDepartmentPolicy(
dto: PermissionPolicyDepartmentDto | PermissionPolicyObject,
fallbackCampusId = '',
): DepartmentPermissionPolicyRecord | null {
const source = asObject(dto)
const departmentId = readString(source, ['departmentId', 'departmentID', 'id'])
if (!departmentId) {
return null
}
const ownPermission = readPolicyLevel(
source,
['ownPermission', 'ownLevel'],
['own', 'ownDocuments', 'ownDocument', 'ownEnabled'],
)
const commonPermission = readPolicyLevel(
source,
['commonPermission', 'commonLevel'],
['common', 'commonDocuments', 'commonDocument', 'commonEnabled'],
)
const otherPermission = readPolicyLevel(
source,
['otherPermission', 'otherLevel'],
['other', 'otherDocuments', 'otherDocument', 'otherEnabled'],
)
return {
departmentId,
code: readString(source, ['code', 'departmentCode'], departmentId),
name: readString(source, ['name', 'departmentName'], departmentId),
campusId: readString(source, ['campusId', 'campusID'], fallbackCampusId),
own: departmentFlagsForPolicyLevel(ownPermission),
common: departmentFlagsForPolicyLevel(commonPermission),
other: departmentFlagsForPolicyLevel(otherPermission),
ownPermission,
commonPermission,
otherPermission,
version: readVersion(source),
}
}
function normalizePermissionPolicies(
data: PermissionPoliciesResponseDto | PermissionPolicyObject,
requestedCampusId = '',
): PermissionPoliciesRecord {
const source = asObject(data)
const campusId = readString(source, ['campusId', 'campusID'], requestedCampusId)
const roleDtos = asArray(source.roles ?? source.rolePolicies ?? source.rolePermissionPolicies)
const departmentDtos = asArray(
source.departments ?? source.departmentPolicies ?? source.departmentPermissionPolicies,
)
const roles = roleDtos
.map((role) => mapRolePolicy(role as PermissionPolicyObject, '', campusId))
.filter((role): role is RolePermissionPolicyRecord => Boolean(role))
const departments = departmentDtos
.map((department) => mapDepartmentPolicy(department as PermissionPolicyObject, campusId))
.filter((department): department is DepartmentPermissionPolicyRecord => Boolean(department))
return {
campusId,
roles,
departments,
templateCount: readNumber(source, ['templateCount', 'templatesCount', 'count']) ?? 0,
}
}
function mapUpdatedRolePolicy(
data: PermissionPolicyRoleDto | PermissionPolicyObject,
roleId: string,
previous: RolePermissionPolicyRecord,
) {
return (
mapRolePolicy(data, roleId, previous.campusId) ?? {
...previous,
roleId,
}
)
}
function mapUpdatedDepartmentPolicy(
data: PermissionPolicyDepartmentDto | PermissionPolicyObject,
departmentId: string,
previous: DepartmentPermissionPolicyRecord,
) {
return (
mapDepartmentPolicy(data, previous.campusId) ?? {
...previous,
departmentId,
}
)
}
export async function getPermissionPolicies(campusId?: string): Promise<PermissionPoliciesRecord> {
if (useMockData) {
return {
...mockPermissionPolicies,
campusId: campusId ?? mockPermissionPolicies.campusId,
roles: mockPermissionPolicies.roles.map((role) => ({ ...role })),
departments: mockPermissionPolicies.departments.map((department) => ({ ...department })),
}
}
const response = await request.get<ApiResponse<PermissionPoliciesResponseDto>>(
'/v1/templates/permission-policies',
{ params: campusId ? { campusId } : undefined },
)
return normalizePermissionPolicies(unwrapApiResponse(response), campusId)
}
export async function updateRolePermissionPolicy(
roleId: string,
payload: UpdateRolePermissionPolicyRequest,
previous?: RolePermissionPolicyRecord,
): Promise<RolePermissionPolicyRecord> {
if (useMockData) {
const role = mockPermissionPolicies.roles.find((item) => item.roleId === roleId)
if (!role) {
throw new Error('角色默认策略不存在')
}
Object.assign(role, flagsForPolicyLevel(payload.permissionLevel), {
permissionLevel: payload.permissionLevel,
campusId: payload.campusId,
version: (role.version ?? 0) + 1,
})
return { ...role }
}
const response = await request.put<ApiResponse<PermissionPolicyRoleDto>>(
`/v1/templates/permission-policies/roles/${encodeURIComponent(roleId)}`,
payload,
)
return mapUpdatedRolePolicy(
unwrapApiResponse(response),
roleId,
previous ?? {
roleId,
campusId: payload.campusId,
code: roleId,
name: roleId,
description: '—',
userCount: null,
...flagsForPolicyLevel(payload.permissionLevel),
permissionLevel: payload.permissionLevel,
version: payload.expectedVersion,
},
)
}
export async function updateDepartmentPermissionPolicy(
departmentId: string,
payload: UpdateDepartmentPermissionPolicyRequest,
previous?: DepartmentPermissionPolicyRecord,
): Promise<DepartmentPermissionPolicyRecord> {
if (useMockData) {
const department = mockPermissionPolicies.departments.find(
(item) => item.departmentId === departmentId,
)
if (!department) {
throw new Error('科室默认策略不存在')
}
Object.assign(department, {
own: departmentFlagsForPolicyLevel(payload.ownPermission),
common: departmentFlagsForPolicyLevel(payload.commonPermission),
other: departmentFlagsForPolicyLevel(payload.otherPermission),
ownPermission: payload.ownPermission,
commonPermission: payload.commonPermission,
otherPermission: payload.otherPermission,
campusId: payload.campusId,
version: (department.version ?? 0) + 1,
})
return { ...department }
}
const response = await request.put<ApiResponse<PermissionPolicyDepartmentDto>>(
`/v1/templates/permission-policies/departments/${encodeURIComponent(departmentId)}`,
payload,
)
return mapUpdatedDepartmentPolicy(
unwrapApiResponse(response),
departmentId,
previous ?? {
departmentId,
code: departmentId,
name: departmentId,
campusId: payload.campusId,
own: departmentFlagsForPolicyLevel(payload.ownPermission),
common: departmentFlagsForPolicyLevel(payload.commonPermission),
other: departmentFlagsForPolicyLevel(payload.otherPermission),
ownPermission: payload.ownPermission,
commonPermission: payload.commonPermission,
otherPermission: payload.otherPermission,
version: payload.expectedVersion,
},
)
}
export function getDocumentPermissions(query: PermissionQuery): Promise<PermissionListResponse> {
if (!useMockData) {
return Promise.reject(
+2 -1
View File
@@ -17,7 +17,8 @@ import type {
TemplateVersionResponseDto,
} from './types'
const useMockData = import.meta.env.VITE_USE_MOCK !== 'false'
// 仅显式开启 VITE_USE_MOCK=true 时使用演示数据,避免联调环境因未注入变量而误入 Mock 分支。
const useMockData = import.meta.env.VITE_USE_MOCK === 'true'
export const isDocumentMockEnabled = useMockData
@@ -13,7 +13,8 @@ import type {
UpdateDepartmentRequest,
} from './types'
const useMockData = import.meta.env.VITE_USE_MOCK !== 'false'
// 仅显式开启 VITE_USE_MOCK=true 时使用演示数据,避免联调环境因未注入变量而误入 Mock 分支。
const useMockData = import.meta.env.VITE_USE_MOCK === 'true'
function getRecords(data: BackendCollection<OrganizationResponseDto>) {
return Array.isArray(data) ? data : (data.records ?? data.items ?? data.content ?? [])
@@ -14,7 +14,8 @@ import type {
UpdateApiPermissionRequest,
} from './types'
const useMockData = import.meta.env.VITE_USE_MOCK !== 'false'
// 仅显式开启 VITE_USE_MOCK=true 时使用演示权限目录。
const useMockData = import.meta.env.VITE_USE_MOCK === 'true'
export const isApiPermissionMockEnabled = useMockData
+2 -1
View File
@@ -14,7 +14,8 @@ import type {
UpdateRoleRequest,
} from './types'
const useMockData = import.meta.env.VITE_USE_MOCK !== 'false'
// 仅显式开启 VITE_USE_MOCK=true 时使用演示数据,避免联调环境因未注入变量而误入 Mock 分支。
const useMockData = import.meta.env.VITE_USE_MOCK === 'true'
const fallbackColors = ['#6b5bd2', '#0e6e8c', '#0f9d6c', '#d9821f', '#c65d5d']
+112
View File
@@ -538,6 +538,118 @@ export interface RolePermissionsUpdateRequest {
expectedVersion?: number
}
export type PermissionPolicyLevel = 'NONE' | 'VIEW' | 'USE' | 'MAINTAIN'
/** 默认文档权限策略的后端 DTO。字段保留常见别名,由 API 边界统一为页面模型。 */
export interface PermissionPolicyRoleDto {
roleId?: string | null
id?: string | null
campusId?: string | null
code?: string | null
roleCode?: string | null
name?: string | null
roleName?: string | null
description?: string | null
userCount?: number | null
view?: unknown
canView?: unknown
viewEnabled?: unknown
use?: unknown
canUse?: unknown
useEnabled?: unknown
maintain?: unknown
canMaintain?: unknown
maintainEnabled?: unknown
permissionLevel?: PermissionPolicyLevel | string | null
maxPermissionLevel?: PermissionPolicyLevel | string | null
version?: number | null
}
export interface PermissionPolicyDepartmentDto {
departmentId?: string | null
id?: string | null
code?: string | null
departmentCode?: string | null
name?: string | null
departmentName?: string | null
campusId?: string | null
ownPermission?: PermissionPolicyLevel | string | null
commonPermission?: PermissionPolicyLevel | string | null
otherPermission?: PermissionPolicyLevel | string | null
own?: unknown
ownDocuments?: unknown
ownDocument?: unknown
ownEnabled?: unknown
common?: unknown
commonDocuments?: unknown
commonDocument?: unknown
commonEnabled?: unknown
other?: unknown
otherDocuments?: unknown
otherDocument?: unknown
otherEnabled?: unknown
version?: number | null
}
export interface PermissionPoliciesResponseDto {
campusId?: string | null
roles?: PermissionPolicyRoleDto[] | null
rolePolicies?: PermissionPolicyRoleDto[] | null
departments?: PermissionPolicyDepartmentDto[] | null
departmentPolicies?: PermissionPolicyDepartmentDto[] | null
templateCount?: number | null
templatesCount?: number | null
}
export interface RolePermissionPolicyRecord {
roleId: string
campusId: string
code: string
name: string
description: string
userCount: number | null
view: boolean
use: boolean
maintain: boolean
permissionLevel: PermissionPolicyLevel
version?: number
}
export interface DepartmentPermissionPolicyRecord {
departmentId: string
version?: number
code: string
name: string
campusId: string
own: boolean
common: boolean
other: boolean
ownPermission: PermissionPolicyLevel
commonPermission: PermissionPolicyLevel
otherPermission: PermissionPolicyLevel
}
export interface PermissionPoliciesRecord {
campusId: string
roles: RolePermissionPolicyRecord[]
departments: DepartmentPermissionPolicyRecord[]
templateCount: number
}
export interface UpdateRolePermissionPolicyRequest {
campusId: string
permissionLevel: PermissionPolicyLevel
expectedVersion?: number
}
export interface UpdateDepartmentPermissionPolicyRequest {
campusId: string
ownPermission: PermissionPolicyLevel
commonPermission: PermissionPolicyLevel
otherPermission: PermissionPolicyLevel
expectedVersion?: number
}
export interface PasswordResetRequest {
newPassword: string
forceChangeOnNextLogin?: boolean
+2 -1
View File
@@ -22,7 +22,8 @@ import type {
UserRolesUpdateRequest,
} from './types'
const useMockData = import.meta.env.VITE_USE_MOCK !== 'false'
// 仅显式开启 VITE_USE_MOCK=true 时使用演示数据,避免联调环境因未注入变量而误入 Mock 分支。
const useMockData = import.meta.env.VITE_USE_MOCK === 'true'
export const isUserMockEnabled = useMockData