修复工作人员资质关联与角色过滤

按安装、配送、运维菜单显式过滤工作人员,锁定资质所有者并使用姓名和角色回显。

扩展工作人员状态与多角色查询,补充权限校验、回归测试、操作日志和项目文档。
This commit is contained in:
czl231
2026-08-11 19:15:53 +08:00
parent 4b1f045806
commit 74c5e34275
20 changed files with 976 additions and 100 deletions

View File

@@ -3,6 +3,7 @@
* 版本v1.1.0
*/
import dayjs from 'dayjs';
import { staffRoleLabel } from './resource-staff-relation';
import type { ResourceField, ResourceUiDefinition } from './resources';
import type { RecordPageMode, ResourceRow } from './resource-page-rules';
@@ -110,6 +111,14 @@ export function optionLabel(option: ResourceRow) {
);
}
/** 工作人员关系额外展示角色,其他关系保持原有可读名称。 */
export function relationOptionLabel(field: ResourceField, option: ResourceRow) {
const label = optionLabel(option);
return field.relation === '/staff_account'
? `${label}${staffRoleLabel(option.role_code)}`
: label;
}
function relationLabel(
field: ResourceField,
identity: string,
@@ -119,6 +128,9 @@ function relationLabel(
(option) => String(option.identity) === identity,
);
if (!match) return identity;
if (field.relation === '/staff_account') {
return relationOptionLabel(field, match);
}
return field.displayRelationLabel
? optionLabel(match)
: `${optionLabel(match)} · ${identity}`;
@@ -161,7 +173,10 @@ export function displayResourceField(
field: ResourceField,
row: ResourceRow,
relationOptions: Record<string, ResourceRow[]>,
fieldOptions: Record<string, Array<{ label: string; value: string | number }>> = {},
fieldOptions: Record<
string,
Array<{ label: string; value: string | number }>
> = {},
) {
const value = row[field.key] ?? row[`${field.key}_masked`];
if (value == null || value === '') return field.emptyText ?? '-';
@@ -169,9 +184,7 @@ export function displayResourceField(
Object.prototype.hasOwnProperty.call(fieldOptions, field.key) ||
Boolean(field.options);
const options = fieldOptions[field.key] ?? field.options;
const option = options?.find(
(item) => String(item.value) === String(value),
);
const option = options?.find((item) => String(item.value) === String(value));
if (option) return option.label;
if (hasOptionSource && field.unknownValueLabel) {
return `${field.unknownValueLabel}${String(value)}`;

View File

@@ -0,0 +1,136 @@
/**
* 功能:定义工作人员关联字段的显式过滤、角色展示与资质来源校验策略。
* 版本v1.0.0
*/
export const staffRoleCodes = ['installer', 'delivery', 'operations'] as const;
export type StaffRoleCode = (typeof staffRoleCodes)[number];
export type StaffRelationPolicy = {
roles: readonly StaffRoleCode[] | 'context';
enabledOnly?: boolean;
workStatus?: 'on_duty' | 'off_duty';
lockPrefilled?: boolean;
showIdentityCopy?: boolean;
};
export type StaffRelationContext = {
staffType?: string;
};
type StaffRelationField = {
relation?: string;
staffRelation?: StaffRelationPolicy;
};
type StaffOwner = {
identity?: unknown;
role_code?: unknown;
status?: unknown;
};
const roleLabels: Record<StaffRoleCode, string> = {
installer: '安装人员',
delivery: '配送人员',
operations: '运维人员',
};
/** 将外部字符串收敛为工作人员闭集角色。 */
export function normalizeStaffRole(value: unknown): StaffRoleCode | '' {
const role = String(value ?? '').trim();
return staffRoleCodes.includes(role as StaffRoleCode)
? (role as StaffRoleCode)
: '';
}
/** 返回工作人员角色的中文名称。 */
export function staffRoleLabel(value: unknown) {
const role = normalizeStaffRole(value);
return role ? roleLabels[role] : '未知角色';
}
/**
* 为工作人员关联字段构造服务端过滤条件;缺少显式策略时立即失败,
* 防止业务字段再次继承隐含的配送人员默认值。
*/
export function staffRelationFilters(
field: StaffRelationField,
context: StaffRelationContext = {},
) {
if (field.relation !== '/staff_account') return {};
if (!field.staffRelation) {
throw new Error('工作人员关联字段缺少显式过滤策略');
}
const roles =
field.staffRelation.roles === 'context'
? [normalizeStaffRole(context.staffType)].filter(
(role): role is StaffRoleCode => Boolean(role),
)
: [...field.staffRelation.roles];
if (!roles.length) throw new Error('工作人员关联字段缺少有效角色上下文');
const filters: Record<string, string> =
roles.length === 1
? { role_code: roles[0] }
: { role_codes: roles.join(',') };
if (field.staffRelation.enabledOnly) filters.status = '1';
if (field.staffRelation.workStatus) {
filters.work_status = field.staffRelation.workStatus;
}
return filters;
}
/** 从站内返回地址中推断工作人员菜单角色,仅用作显式参数缺失时的回退。 */
export function staffRoleFromReturnPath(returnTo: string) {
let current = returnTo;
for (let depth = 0; depth < 3 && current; depth += 1) {
const url = new URL(current, 'http://codex.local');
const queryRole = normalizeStaffRole(url.searchParams.get('staff_type'));
if (queryRole) return queryRole;
const pathRole = [
['/staff/installers', 'installer'],
['/staff/delivery', 'delivery'],
['/staff/operations', 'operations'],
].find(([path]) => url.pathname.startsWith(path))?.[1];
if (pathRole) return pathRole as StaffRoleCode;
current = url.searchParams.get('return_to') ?? '';
}
return '';
}
/** 解析资质页面角色来源,并拒绝显式参数与返回路径互相冲突。 */
export function resolveCredentialStaffRole(
explicitRole: unknown,
returnTo: string,
) {
const explicit = normalizeStaffRole(explicitRole);
const inferred = staffRoleFromReturnPath(returnTo);
if (explicit && inferred && explicit !== inferred) {
return { role: '' as const, error: '工作人员角色与来源菜单不一致' };
}
const role = explicit || inferred;
return role
? { role, error: '' }
: {
role: '' as const,
error: '缺少工作人员角色来源,请从工作人员页面进入',
};
}
/** 校验资质所有者与经服务端查询得到的真实人员记录是否一致。 */
export function credentialOwnerValidationMessage(
ownerIdentity: string,
expectedRole: StaffRoleCode,
owner: StaffOwner | undefined,
) {
if (!ownerIdentity) return '缺少工作人员唯一标识,请从工作人员页面进入';
if (!owner) return '工作人员不存在、已归档或无权访问';
if (String(owner.identity ?? '') !== ownerIdentity) {
return '工作人员唯一标识与查询结果不一致';
}
if (normalizeStaffRole(owner.role_code) !== expectedRole) {
return '工作人员角色已变化,请从当前角色菜单重新进入';
}
if (Number(owner.status) === 3) return '已归档工作人员不能新增资质';
return '';
}

View File

@@ -41,6 +41,7 @@ export type ResourceField = {
readonlyOnCreate?: boolean;
unknownValueLabel?: string;
relationLinkage?: ResourceRelationLinkage;
staffRelation?: import('./resource-staff-relation').StaffRelationPolicy;
};
export type DetailAction = {
@@ -289,8 +290,13 @@ function f(key: string, options: Partial<ResourceField> = {}): ResourceField {
return { key, label, type, ...options };
}
function relation(key: string, resource: string, required = false): ResourceField {
return f(key, { type: 'identity', relation: resource, required });
function relation(
key: string,
resource: string,
required = false,
options: Partial<ResourceField> = {},
): ResourceField {
return f(key, { ...options, type: 'identity', relation: resource, required });
}
/** 创建固定管理员角色字段,页面展示中文名称,接口仍使用稳定编码。 */
@@ -349,10 +355,10 @@ export const resources: ResourceUiDefinition[] = [
{ ...define('delivery_basic', '配送点管理', 'writable', [f('delivery_code', { required: true }), f('name', { required: true }), f('gas_basic_identity', { label: '气站', listLabel: '气站名称', type: 'identity', relation: '/gas_basic', displayRelationLabel: true, emptyText: '平台直属', placeholder: '请选择气站,留空表示平台直属' }), f('principal'), f('address')]), accountManagement: { resource: '/delivery_account', relationKey: 'delivery_basic_identity', title: '配送点账户' }, walletOwnerType: 'delivery' },
define('delivery_account', '配送点账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), fixedAdminRole('配送点管理员'), relation('delivery_basic_identity', '/delivery_basic', true)]),
{ ...define('staff_account', '工作人员', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('role_code', { required: true, type: 'select', options: [{ label: '安装人员', value: 'installer' }, { label: '配送人员', value: 'delivery' }, { label: '运维人员', value: 'operations' }] }), f('gas_basic_identity', { label: '所属气站', type: 'identity', relation: '/gas_basic' }), f('delivery_basic_identity', { label: '所属配送点', type: 'identity', relation: '/delivery_basic', relationLinkage: { parentKey: 'gas_basic_identity', optionParentKey: 'gas_basic_identity', filterKey: 'gas_basic_identities', backfillParent: true } }), f('work_status', { type: 'select', options: [{ label: '在岗', value: 'on_duty' }, { label: '离岗', value: 'off_duty' }] })]), walletOwnerType: 'staff' },
define('staff_credential', '工作人员资质', 'writable', [relation('staff_account_identity', '/staff_account', true), f('credential_type', { required: true }), f('credential_no'), f('expired_at')]),
define('staff_credential', '工作人员资质', 'writable', [relation('staff_account_identity', '/staff_account', true, { staffRelation: { roles: 'context', lockPrefilled: true, showIdentityCopy: true } }), f('credential_type', { required: true }), f('credential_no'), f('expired_at')]),
{ ...define('user_account', '用户账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('real_name')]), walletOwnerType: 'user' },
define('user_address', '用户地址', 'writable', [relation('user_account_identity', '/user_account', true), f('address', { required: true }), f('longitude'), f('latitude'), f('is_default')]),
define('user_service_relation', '用户服务关系', 'writable', [relation('user_account_identity', '/user_account', true), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('staff_account_identity', '/staff_account')]),
define('user_service_relation', '用户服务关系', 'writable', [relation('user_account_identity', '/user_account', true), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('staff_account_identity', '/staff_account', false, { staffRelation: { roles: ['installer', 'delivery', 'operations'], enabledOnly: true } })]),
define('producer_account', '生产商管理', 'writable', [f('producer_code', { required: true }), f('name', { required: true }), f('credit_code'), f('principal'), f('phone'), f('address'), f('username', { required: true }), f('password', { required: true }), f('display_name'), f('role_code'), f('remark')]),
define('product_type', '智能气阀类型', 'editable', [f('code', { required: true }), f('name', { required: true })]),
@@ -374,7 +380,7 @@ export const resources: ResourceUiDefinition[] = [
]),
define('gasorder_contract_revision', '合同修订记录', 'readonly', []),
define('gasorder_basic', '气体配送订单', 'append_only', [f('request_no', { required: true }), relation('gasorder_contract_identity', '/gasorder_contract', true), f('creator_type', { required: true, type: 'select', options: [{ label: '用户', value: 'user' }, { label: '工作人员', value: 'staff' }, { label: '配送站', value: 'delivery' }, { label: '气站', value: 'gas' }] }), f('creator_identity', { required: true }), relation('user_address_identity', '/user_address', true), f('gasorder_contract_product_identities', { required: true, type: 'identity-list', relation: '/gasorder_contract_product' }), f('contact_name', { required: true }), f('contact_phone', { required: true }), f('discount_amount'), f('remark')], 'list', [
{ name: '分配订单', resource: '/gasorder_basic/:identity/assign', fields: [relation('delivery_basic_identity', '/delivery_basic', true), relation('staff_account_identity', '/staff_account', true), ...reason], visibleFor: { field: 'order_status', values: [16, 18] } },
{ name: '分配订单', resource: '/gasorder_basic/:identity/assign', fields: [relation('delivery_basic_identity', '/delivery_basic', true), relation('staff_account_identity', '/staff_account', true, { staffRelation: { roles: ['delivery'], enabledOnly: true, workStatus: 'on_duty' } }), ...reason], visibleFor: { field: 'order_status', values: [16, 18] } },
{ name: '开始罐装', resource: '/gasorder_basic/:identity/filling', fields: reason, visibleFor: { field: 'order_status', values: [18] } },
{ name: '待配送', resource: '/gasorder_basic/:identity/ready', fields: reason, visibleFor: { field: 'order_status', values: [19] } },
{ name: '开始配送', resource: '/gasorder_basic/:identity/delivering', fields: reason, visibleFor: { field: 'order_status', values: [20] } },