Archived
三面拆分⑲:审批点卸岗位 —— 负责人一律运营时分配,绑员工组,实例记 assignee
模型(用户定):审批点不绑岗位;节点负责人是运营时分配的,绑定到员工组。 - 契约:approval 节点删 approver(功能属性只剩驳回走向/催办/委托消息); CLOSURE_VIOLATION_KINDS 删 missing-owner-position(岗位不入编排,无岗可核); 治理审批列表删 approverPosition 字段 - 运行时统一口径(活动点=审批点):工作流绑了员工组按组轮值(组员并集最闲者先), 没绑组全员轮值;staffId 稳定排序保证确定性,组员全不在岗退回全员。 ADR-011「绑定先于策略」随岗位退场 —— 工作组快照的成员集合成为轮值池 - 实例记 assignee(迁移 0014 node_instance.assignee_id):分派结果是流转事实 —— 审批箱收件(i.assigneeId === me)、裁决权(同一判据)、员工面链条报实际承接人、 治理台审批列表报人,全部按它认,不再有岗位比对 - closure 卸 positions 入参;desk/desk-approval/governance-ops 三路由改口; web 属性卡删「审批岗位」选择与警示、positions 管线拆除;种子 A5 卸 PM; 导出依赖文件审批段只留编排语义 测试:closure 25、save-package 6、seed 8、graph-to-pkg 5、端到端八例真库全绿; 五闸退出码全过。库:迁移 + v4 就地同步(唯一生效版)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -38,17 +38,11 @@ export function registerDeskApprovalRoutes(app: Hono<AppEnv>, container: Contain
|
||||
const node = pkg.nodes.find((n) => n.id === i.nodeId);
|
||||
if (node?.kind !== 'approval') return [];
|
||||
/*
|
||||
* 谁的审批,要和**真正派单时的口径一致**(ADR-011:绑定先于策略)。
|
||||
*
|
||||
* 从前这里只比岗位码:`node.approver.position !== me.position.code` 就滤掉。
|
||||
* 可引擎派审批时先看工作组绑定(`workGroup[岗位] → 员工`),装了组的单
|
||||
* 实际派给的是被绑定的那个人 —— 于是出现「审批派给了我、我的审批箱里没有」:
|
||||
* 单子谁也看不见,流永远卡在审批点(实测:工作组把 PM 绑到本人,审批箱空)。
|
||||
* `toChain` 早为同一件事修过一次,这里是漏的那一处。
|
||||
* 谁的审批,要和**真正派单时的口径一致**:审批人是运营时分配的
|
||||
* (员工组轮值 / 全员轮值),分派结果记在实例的 assignee 上 ——
|
||||
* 收件按它认人,不再有岗位可比(编排不带岗位,2026-07-28 第五轮)。
|
||||
*/
|
||||
const bound = w.workGroup?.[node.approver.position];
|
||||
const mine = bound === undefined ? node.approver.position === me.position.code : bound === me.id;
|
||||
if (!mine) return [];
|
||||
if (i.assigneeId !== me.id) return [];
|
||||
return [
|
||||
{
|
||||
id: `${w.id}:${i.nodeId}#${i.instance}`,
|
||||
@@ -94,7 +88,6 @@ export function registerDeskApprovalRoutes(app: Hono<AppEnv>, container: Contain
|
||||
const workflowId = c.req.param('workflowId');
|
||||
const nodeId = c.req.param('nodeId');
|
||||
|
||||
// 审批权按岗位校验:节点声明的审批岗位必须是我的岗位
|
||||
const workflow = await container.process.getWorkflow(workflowId);
|
||||
const pkg = await container.process.getPackage(workflow.packageId, workflow.packageVersion);
|
||||
const node = pkg?.nodes.find((n) => n.id === nodeId);
|
||||
@@ -103,19 +96,13 @@ export function registerDeskApprovalRoutes(app: Hono<AppEnv>, container: Contain
|
||||
return c.json(body, 404);
|
||||
}
|
||||
/*
|
||||
* 裁决权也要按**派单口径**核,不是只比岗位码 —— 与上面的收件口径同一条规矩。
|
||||
* 只比岗位的话:装了工作组的单看得见却裁不了(403),人干瞪眼、流卡死。
|
||||
* 裁决权与收件同一口径:这一实例分派给谁,谁裁 —— 实例 assignee 是唯一判据
|
||||
* (审批人运营时分配,编排里没有岗位可比)。
|
||||
*/
|
||||
const boundApprover = workflow.workGroup?.[node.approver.position];
|
||||
const mayDecide = boundApprover === undefined ? node.approver.position === me.position.code : boundApprover === me.id;
|
||||
if (!mayDecide) {
|
||||
const body: ApiError = {
|
||||
code: 'forbidden',
|
||||
message:
|
||||
boundApprover === undefined
|
||||
? `审批点卡在 ${node.approver.position} 工位,岗位 ${me.position.code} 不得裁决`
|
||||
: `本单的 ${node.approver.position} 由工作组绑定给了他人,你不得裁决`,
|
||||
};
|
||||
const instances = await container.process.instancesOf(workflowId);
|
||||
const current = [...instances].reverse().find((i) => i.nodeId === nodeId && i.activated);
|
||||
if (current?.assigneeId !== me.id) {
|
||||
const body: ApiError = { code: 'forbidden', message: '本单分派给了他人,你不得裁决' };
|
||||
return c.json(body, 403);
|
||||
}
|
||||
|
||||
|
||||
@@ -94,38 +94,21 @@ export function registerDeskRoutes(app: Hono<AppEnv>, container: Container): voi
|
||||
return await Promise.all(
|
||||
nodes.map(async (n) => {
|
||||
/*
|
||||
* 报谁,要和**真正派单时的口径一致**。
|
||||
* 审批点:ADR-011 绑定先于策略 —— 先看工作组绑定,再退回岗位解析。
|
||||
* 活动点:契约不带岗位(分派归运营)—— 起了单的报**实际承接人**(任务上的),
|
||||
* 还没起的如实报「轮值待派」,不编造具体承接人。
|
||||
* 报谁,要和**真正派单时的口径一致**:负责人是运营时分配的(员工组轮值 /
|
||||
* 全员轮值),分派结果记在实例 assignee 上 —— 起了单的报实际承接人,
|
||||
* 还没起的如实报「轮值待派」,不编造具体承接人。活动点与审批点同一口径。
|
||||
*/
|
||||
let who;
|
||||
let fallbackName = '轮值待派';
|
||||
let fallbackRole = '全员轮值';
|
||||
if (n.kind === 'approval') {
|
||||
const spec = n.approver;
|
||||
fallbackName = spec.position;
|
||||
fallbackRole = spec.position;
|
||||
const bound = workflow.workGroup?.[spec.position];
|
||||
const boundStaff =
|
||||
bound === undefined
|
||||
? undefined
|
||||
: await container.org.getStaff(staffIdSchema.parse(bound)).catch(() => undefined);
|
||||
// 岗位唯一解析(sole)即报人名;否则只报岗位
|
||||
const holders =
|
||||
boundStaff !== undefined ? [] : await container.org.staffOfPosition(spec.position).catch(() => []);
|
||||
who = boundStaff ?? (holders.length === 1 ? holders[0] : undefined);
|
||||
} else {
|
||||
const inst = instances.find((x) => x.nodeId === n.id && x.taskId !== undefined);
|
||||
const owned = inst?.taskId === undefined ? undefined : await container.task.get(taskIdSchema.parse(inst.taskId)).catch(() => undefined);
|
||||
who = owned === undefined ? undefined : await container.org.getStaff(owned.ownerId).catch(() => undefined);
|
||||
}
|
||||
const inst = [...instances].reverse().find((x) => x.nodeId === n.id && x.assigneeId !== undefined);
|
||||
const who =
|
||||
inst?.assigneeId === undefined
|
||||
? undefined
|
||||
: await container.org.getStaff(staffIdSchema.parse(inst.assigneeId)).catch(() => undefined);
|
||||
return {
|
||||
nodeId: n.id,
|
||||
label: n.label,
|
||||
kind: n.kind === 'approval' ? ('approval' as const) : ('activity' as const),
|
||||
ownerName: who?.name ?? fallbackName,
|
||||
ownerRole: who?.position.name ?? fallbackRole,
|
||||
ownerName: who?.name ?? '轮值待派',
|
||||
ownerRole: who?.position.name ?? '运营分派',
|
||||
state:
|
||||
n.id === task.nodeId
|
||||
? ('current' as const)
|
||||
|
||||
@@ -185,15 +185,17 @@ export function registerGovernanceOpsRoutes(app: Hono<AppEnv>, container: Contai
|
||||
.filter((i) => i.activated && pkg.nodes.find((n) => n.id === i.nodeId)?.kind === 'approval')
|
||||
.map(async (i) => {
|
||||
const node = pkg.nodes.find((n) => n.id === i.nodeId);
|
||||
const position = node?.kind === 'approval' ? node.approver.position : '';
|
||||
const holders = position === '' ? [] : await container.org.staffOfPosition(position);
|
||||
// 审批人是运营时分配的,实例 assignee 即事实 —— 没派到的如实「—」
|
||||
const assignee =
|
||||
i.assigneeId === undefined
|
||||
? undefined
|
||||
: await container.org.getStaff(staffIdSchema.parse(i.assigneeId)).catch(() => undefined);
|
||||
return {
|
||||
id: `${w.id}:${i.nodeId}#${i.instance}`,
|
||||
workflowId: w.id,
|
||||
workflowSubject: w.subject,
|
||||
title: `审批:${node?.label ?? i.nodeId}`,
|
||||
approverName: holders[0]?.name ?? '—',
|
||||
approverPosition: position,
|
||||
approverName: assignee?.name ?? '—',
|
||||
// 终局唯放行:released = 通过;其余终态 = 退回;未终态 = 待审批
|
||||
state:
|
||||
i.terminal === undefined
|
||||
|
||||
@@ -63,8 +63,6 @@ export const MVP_PACKAGE: ProcessPackage = processPackageSchema.parse({
|
||||
id: 'A5',
|
||||
kind: 'approval',
|
||||
label: '定价审批',
|
||||
sub: 'PM',
|
||||
approver: { position: 'PM', strategy: 'sole' },
|
||||
/*
|
||||
* 驳回走向声明式,不由临场决定:退回 A4 重新定价。
|
||||
* 图上由 A5 自己的 `verdict = "reject"` 条件出边(E10)**显式表达同一件事**,
|
||||
@@ -236,7 +234,7 @@ export const MVP_PACKAGE: ProcessPackage = processPackageSchema.parse({
|
||||
{ id: 'A2', kind: 'activity', label: '替代选型', code: 'A2', x: 300, y: 70 },
|
||||
{ id: 'A3', kind: 'activity', label: '寻源询价', code: 'A3', x: 480, y: 70 },
|
||||
{ id: 'A4', kind: 'activity', label: '成本定价', code: 'A4', x: 120, y: 320 },
|
||||
{ id: 'A5', kind: 'activity', label: '定价审批', code: 'A5', owner: 'PM', flavor: 'approval', x: 300, y: 320 },
|
||||
{ id: 'A5', kind: 'activity', label: '定价审批', code: 'A5', flavor: 'approval', x: 300, y: 320 },
|
||||
{ id: 'A6', kind: 'activity', label: '报价议价', code: 'A6', x: 480, y: 320 },
|
||||
{ id: 'A7', kind: 'activity', label: '情报归档', code: 'A7', x: 640, y: 420 },
|
||||
{ id: 'end_won', kind: 'end', label: '转订单履约', x: 640, y: 330, lx: 650, ly: 360 },
|
||||
|
||||
Reference in New Issue
Block a user