强化配送订单合同调度约束

This commit is contained in:
czl231
2026-08-15 19:19:15 +08:00
parent 963e497bf3
commit 42a4fc2f41
15 changed files with 501 additions and 52 deletions

View File

@@ -26,6 +26,7 @@
"gasorder-contract-candidates:check": "node scripts/check-gasorder-contract-candidates.mjs",
"gasorder-contract-products:check": "node scripts/check-gasorder-contract-product-display.mjs",
"gasorder-creator:check": "node scripts/check-gasorder-creator-linkage.mjs",
"gasorder-assignment:check": "node scripts/check-gasorder-assignment-linkage.mjs",
"gasorder-list-display:check": "node scripts/check-gasorder-list-display.mjs",
"gasorder-status-display:check": "node scripts/check-gasorder-status-display.mjs",
"product-ownership:check": "node scripts/check-product-ownership-action.mjs",

View File

@@ -0,0 +1,22 @@
/**
* 功能:静态检查配送订单分配动作是否保留合同履约范围、配送点级联和有效资质约束。
* 版本v1.0.0
*/
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const resources = readFileSync(new URL('../src/api/resources.ts', import.meta.url), 'utf8');
const dialog = readFileSync(new URL('../src/views/resource/ResourceActionDialog.vue', import.meta.url), 'utf8');
const staffPolicy = readFileSync(new URL('../src/api/resource-staff-relation.ts', import.meta.url), 'utf8');
assert.match(resources, /resource: '\/gasorder_basic\/:identity\/assign'/, '缺少订单分配动作');
assert.match(resources, /relationFilters: \{ status: '1' \}/, '配送点候选必须限定启用状态');
assert.match(resources, /validCredentialOnly: true/, '配送人员候选必须限定有效资质');
assert.match(dialog, /lockedAssignmentDeliveryIdentity/, '合同或订单已有配送点时必须锁定');
assert.match(dialog, /assignmentDeliveryFilters/, '配送点候选必须按订单气站过滤');
assert.match(dialog, /assignmentStaffFilters/, '配送人员候选必须按气站和配送点过滤');
assert.match(dialog, /form\.staff_account_identity = undefined/, '切换配送点必须清空旧配送人员');
assert.match(dialog, /initializeGasorderAssignment/, '分配弹窗不得预加载全平台候选');
assert.match(staffPolicy, /valid_credential_only = '1'/, '有效资质策略必须转换为服务端筛选参数');
console.log('配送订单分配联动检查通过:配送点受合同与气站约束,配送人员按点和有效资质筛选。');

View File

@@ -1,6 +1,6 @@
/**
* 功能:验证工作人员关联字段过滤、资质来源校验和路由上下文传递契约。
* 版本v1.1.0
* 版本v1.2.0
*/
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
@@ -28,12 +28,18 @@ assert.deepEqual(
roles: ['delivery'],
enabledOnly: true,
workStatus: 'on_duty',
validCredentialOnly: true,
},
},
{},
),
{ role_code: 'delivery', status: '1', work_status: 'on_duty' },
'订单分配必须只查询启用且在岗的配送人员',
{
role_code: 'delivery',
status: '1',
work_status: 'on_duty',
valid_credential_only: '1',
},
'订单分配必须只查询启用、在岗且资质有效的配送人员',
);
assert.deepEqual(

View File

@@ -1,6 +1,6 @@
/**
* 功能:定义工作人员关联字段的显式过滤、角色展示与资质来源校验策略。
* 版本v1.0.0
* 版本v1.1.0
*/
export const staffRoleCodes = ['installer', 'delivery', 'operations'] as const;
@@ -11,6 +11,8 @@ export type StaffRelationPolicy = {
roles: readonly StaffRoleCode[] | 'context';
enabledOnly?: boolean;
workStatus?: 'on_duty' | 'off_duty';
/** 仅展示至少拥有一条启用且未过期资质的工作人员。 */
validCredentialOnly?: boolean;
lockPrefilled?: boolean;
showIdentityCopy?: boolean;
organizationScoped?: boolean;
@@ -78,6 +80,9 @@ export function staffRelationFilters(
if (field.staffRelation.workStatus) {
filters.work_status = field.staffRelation.workStatus;
}
if (field.staffRelation.validCredentialOnly) {
filters.valid_credential_only = '1';
}
return filters;
}

View File

@@ -587,7 +587,7 @@ export const resources: ResourceUiDefinition[] = [
parentChangeMessage: '配送合同已变更,请重新选择该合同用户的收货地址',
},
}), f('gasorder_contract_product_identities', { label: '合同气瓶', required: true, type: 'identity-list', listDisplayKey: 'contract_products_summary', listDetailLink: true, emptyText: '未填写', relation: '/gasorder_contract_product', placeholder: '请输入智能气阀名称、设备类型或设备编码搜索', relationOptionDisplay: 'product-name-type', relationEmptyText: '该合同暂无可用气瓶,请先为合同绑定气瓶', relationLinkage: { parentKey: 'gasorder_contract_identity', optionParentKey: '', filterKey: 'contract_identity', filterOnly: true, requiresParent: true, parentChangeMessage: '配送合同已变更,请重新选择该合同可用的气瓶' } }), 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, { staffRelation: { roles: ['delivery'], enabledOnly: true, workStatus: 'on_duty' } }), ...reason], visibleFor: { field: 'order_status', values: [16, 18] } },
{ name: '分配订单', resource: '/gasorder_basic/:identity/assign', fields: [relation('delivery_basic_identity', '/delivery_basic', true, { label: '配送点', placeholder: '请选择合同履约范围内的配送点', relationFilters: { status: '1' } }), relation('staff_account_identity', '/staff_account', true, { label: '配送人员', placeholder: '请先选择配送点', relationEmptyText: '该配送点暂无在岗且资质有效的配送人员', staffRelation: { roles: ['delivery'], enabledOnly: true, workStatus: 'on_duty', validCredentialOnly: true } }), ...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] } },

View File

@@ -1,6 +1,6 @@
<!--
功能承载资源详情页中的审核流转归属调整和菜单分配等短业务操作
版本v1.0.0
版本v1.1.0
-->
<template>
<a-modal
@@ -21,11 +21,15 @@
<a-alert v-if="isProductLifecycleAction" class="action-alert" type="info" show-icon>
当前状态{{ currentProductStatusLabel }}请选择一个不同的目标状态服务端会校验检修和订单占用情况
</a-alert>
<a-alert v-if="isGasorderAssignmentAction" class="action-alert" type="info" show-icon>
{{ assignmentScopeMessage }}
</a-alert>
<a-form :model="form" layout="vertical">
<ResourceFieldForm
v-model="form"
:fields="actionFields"
:required-keys="requiredKeys"
:disabled-keys="disabledKeys"
:relation-options="relations.options"
:relation-loading="relations.loading"
@change-relation="changeRelation"
@@ -62,6 +66,30 @@ const relations = useResourceRelations();
const isProductLifecycleAction = computed(
() => props.action?.resource === '/product_info/:identity/lifecycle',
);
const isGasorderAssignmentAction = computed(
() => props.action?.resource === '/gasorder_basic/:identity/assign',
);
/** 合同指定点始终锁定;无指定点的已分配订单也只能在当前点内改派。 */
const lockedAssignmentDeliveryIdentity = computed(() => {
return String(props.record.delivery_basic_identity ?? '').trim();
});
const disabledKeys = computed(() =>
isGasorderAssignmentAction.value && lockedAssignmentDeliveryIdentity.value
? ['delivery_basic_identity']
: [],
);
const assignmentScopeMessage = computed(() => {
if (
Number(props.record.order_status) === 16 &&
lockedAssignmentDeliveryIdentity.value
) {
return '配送合同已指定履约配送点,本次只能从该配送点的在岗且资质有效人员中分配。';
}
if (lockedAssignmentDeliveryIdentity.value) {
return '订单已完成配送点分配,本次仅允许在当前配送点内改派配送人员。';
}
return '配送合同未指定配送点,请先从合同气站直属的启用配送点中选择,再选择该点配送人员。';
});
const actionFields = computed(() =>
(props.action?.fields ?? []).map((field) =>
isProductLifecycleAction.value && field.key === 'product_status'
@@ -99,13 +127,52 @@ const ownershipKeys = [
'user_account_identity',
];
/** 业务动作字段不启用记录页联动,只沿用普通关联搜索。 */
/** 为订单分配动作保留履约范围,其余业务动作沿用普通关联搜索。 */
function searchRelation(field: ResourceField, keyword: string) {
if (isGasorderAssignmentAction.value) {
if (field.key === 'delivery_basic_identity') {
relations.searchField(field, keyword, {
filters: assignmentDeliveryFilters(),
});
return;
}
if (field.key === 'staff_account_identity') {
const deliveryIdentity = String(
form.delivery_basic_identity ?? '',
).trim();
if (!deliveryIdentity) {
relations.clear(field.relation);
return;
}
relations.searchField(field, keyword, {
filters: assignmentStaffFilters(deliveryIdentity),
});
return;
}
}
relations.searchField(field, keyword);
}
/** 选择智能气阀的新归属时清空其余互斥归属,避免提交非法多重归属。 */
/** 处理订单配送点级联及智能气阀互斥归属,避免提交已经失效的关联值。 */
function changeRelation(field: ResourceField, value: unknown) {
if (
isGasorderAssignmentAction.value &&
field.key === 'delivery_basic_identity'
) {
form.staff_account_identity = undefined;
const deliveryIdentity = String(value ?? '').trim();
const staffField = actionFields.value.find(
(item) => item.key === 'staff_account_identity',
);
if (!staffField || !deliveryIdentity) {
relations.clear('/staff_account');
return;
}
void relations.loadField(staffField, '', {
filters: assignmentStaffFilters(deliveryIdentity),
});
return;
}
if (
!isOwnershipAction.value ||
!ownershipKeys.includes(field.key) ||
@@ -119,6 +186,47 @@ function changeRelation(field: ResourceField, value: unknown) {
}
}
/** 将订单履约气站转换为配送点候选接口的精确筛选参数。 */
function assignmentDeliveryFilters(): Record<string, string> {
const gasIdentity = String(props.record.gas_basic_identity ?? '').trim();
return gasIdentity ? { gas_basic_identities: gasIdentity } : {};
}
/** 配送人员必须同时属于订单气站和当前选择的配送点。 */
function assignmentStaffFilters(deliveryIdentity: string) {
const filters: Record<string, string> = {
delivery_basic_identities: deliveryIdentity,
};
const gasIdentity = String(props.record.gas_basic_identity ?? '').trim();
if (gasIdentity) filters.gas_basic_identities = gasIdentity;
return filters;
}
/** 按合同和订单快照初始化分配动作,禁止预加载全平台候选。 */
async function initializeGasorderAssignment() {
const deliveryField = actionFields.value.find(
(field) => field.key === 'delivery_basic_identity',
);
const staffField = actionFields.value.find(
(field) => field.key === 'staff_account_identity',
);
if (!deliveryField || !staffField) return;
relations.clear('/delivery_basic');
relations.clear('/staff_account');
const lockedIdentity = lockedAssignmentDeliveryIdentity.value;
if (lockedIdentity) {
form.delivery_basic_identity = lockedIdentity;
await relations.ensure('/delivery_basic', lockedIdentity);
await relations.loadField(staffField, '', {
filters: assignmentStaffFilters(lockedIdentity),
});
return;
}
await relations.loadField(deliveryField, '', {
filters: assignmentDeliveryFilters(),
});
}
watch(
() => [props.visible, props.action?.name] as const,
async ([visible]) => {
@@ -128,6 +236,10 @@ watch(
? props.record[field.key]
: undefined;
}
if (isGasorderAssignmentAction.value) {
await initializeGasorderAssignment();
return;
}
await relations.preload(actionFields.value);
if (props.action.resource.includes('/menu')) {
try {