# -*- coding: utf-8 -*- """票 13 第二轮修复: 1) 权限点禁删 61020 确定性链路(演示角色引用→停用角色→资源停用→删除 61020) 2) LeadPool 部门成员用第三个用户(与协作人不同,避开 去重优先级 吞并) """ import io, sys sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') P = 'index.html' t = open(P, encoding='utf-8').read() orig = t def rep(old, new): global t assert old in t, 'ANCHOR MISSING: ' + old[:70] t = t.replace(old, new, 1) # ---- 1a. resPrepare:加演示角色 RD + 授权 + 停用(被引用方全停用,资源才可停用,删除才可达 61020)---- rep(""" ok(1, '演示权限点就绪:id=' + esc(RES.demoId) + '(父 menu=' + esc(RES.parentMenuId) + ',商机管理目录=' + esc(RES.bizCatalogId) + ');禁删靶子(被引用 button 叶子)=' + esc(RES.delTargetId == null ? '未找到' : RES.delTargetId) + ',扫描 button 共 ' + buttons.length + ' 个');""", """ // 确定性 61020 链路:建演示角色 RD → 授权演示点 → 停用 RD。 // 资源停用仅拦「启用中角色」引用;删除第③段拦任何角色引用(61020)——引用方为停用角色才能先停资源再删撞 61020。 const rname = 'e2d-资源演示角色-' + TS; const rpg = await api('/api/roles/page', { method: 'POST', body: qs({ current: 1, size: 100, roleName: rname }) }); for (const old of (rpg.content || []).filter(x => x.roleName === rname)) { try { await api('/api/roles/delete?id=' + old.id, { method: 'POST' }); } catch (e) {} } await api('/api/roles/saveOrUpdate', { method: 'POST', body: qs({ roleName: rname, roleCode: 'e2d_rrole_' + TS, remark: 'a7-parity e2e' }) }); const rcreated = await findRoleByName(rname); RES.roleId = rcreated.id; await api('/api/roles/assign-resources?roleId=' + RES.roleId + '&resourceIds=' + RES.demoId, { method: 'POST' }); await api('/api/roles/disable?id=' + RES.roleId, { method: 'POST' }); ok(1, '演示权限点就绪:id=' + esc(RES.demoId) + '(父 menu=' + esc(RES.parentMenuId) + ');禁删链路就绪:演示角色 RD=' + esc(RES.roleId) + ' 已授权并停用');""") # 删掉旧的扫描逻辑(被上一处替换块引用前的残留段) rep(""" RES.demoId = d && d.id; // 禁删靶子:找首个被角色引用的 BUTTON 叶子(只读扫描;删除走 61020 拒绝路径不动数据) RES.delTargetId = null; const buttons = []; const collect = nodes => { for (const n of (nodes || [])) { if (n.type === 'BUTTON') buttons.push(n); collect(n.children); } }; collect(list); for (const b of buttons) { const refs = await api('/api/resources/references?id=' + b.id).catch(() => []); if ((refs || []).length >= 1) { RES.delTargetId = b.id; break; } } """, """ RES.demoId = d && d.id; RES.roleId = null; """) # RES 状态加 roleId rep("const RES = { demoId: null, parentMenuId: null, bizCatalogId: null };", "const RES = { demoId: null, roleId: null };") # ---- 1b. resDelBlocked:两段断言(启用删=61009 仅停用可删;停资源后删=61020)---- rep("""async function resDelBlocked() { try { if (!RES.delTargetId) throw new Error('步骤0 未找到被引用 button 叶子'); await api('/api/resources/delete?id=' + RES.delTargetId, { method: 'POST' }); fail(1, new Error('被引用资源删除未被拦截')); } catch (e) { if (e.code === 61020) ok(1, '61020 禁删生效:' + esc(e.message)); else fail(1, e); } }""", """async function resDelBlocked() { try { if (!RES.demoId) throw new Error('先执行步骤0'); // 第一段:启用中资源删除 → 61009 仅停用可删 let firstMsg = ''; try { await api('/api/resources/delete?id=' + RES.demoId, { method: 'POST' }); fail(1, new Error('启用中资源删除未被拦截')); return; } catch (e) { if (e.code !== 61009 || !/仅停用/.test(e.message || '')) { fail(1, e); return; } firstMsg = e.message; } // 停用资源(引用方 RD 已停用 → 放行),再删 → 第③段 61020 await api('/api/resources/disable?id=' + RES.demoId, { method: 'POST' }); try { await api('/api/resources/delete?id=' + RES.demoId, { method: 'POST' }); fail(1, new Error('被引用资源删除未被拦截')); } catch (e) { if (e.code === 61020) ok(1, '禁删两段生效:启用中删除=61009(' + esc(firstMsg) + ');停用后删除=61020(' + esc(e.message) + ')'); else fail(1, e); } } catch (e) { fail(1, e); } }""") # ---- 1c. resCleanup:先删 RD(级联清引用),演示点再删 ---- rep("""async function resCleanup() { try { if (!RES.demoId) throw new Error('先执行步骤0'); await api('/api/resources/disable?id=' + RES.demoId, { method: 'POST' }); await api('/api/resources/delete?id=' + RES.demoId, { method: 'POST' }); ok(1, '演示权限点已停用并删除(仅停用可删通路)'); } catch (e) { fail(1, e); } }""", """async function resCleanup() { try { if (!RES.demoId) throw new Error('先执行步骤0'); if (RES.roleId) { try { await api('/api/roles/delete?id=' + RES.roleId, { method: 'POST' }); } catch (e) { log(1, '删 RD: ' + e.message); } } await api('/api/resources/disable?id=' + RES.demoId, { method: 'POST' }); await api('/api/resources/delete?id=' + RES.demoId, { method: 'POST' }); ok(1, '演示角色 RD 已删(级联清引用);演示权限点已停用并删除'); } catch (e) { fail(1, e); } }""") # ---- 2. poolCreate/poolDetail:部门成员用第三个用户 ---- rep("""async function poolCreate() { try { const deptId = await freeDeptId(); const name = 'e2d-演示池-' + TS; await api('/api/rule/pool/saveOrUpdate', { method: 'POST', body: qs({ poolName: name, deptId: String(deptId), ownerUserId: ADMIN_UID, collaboratorUserIds: BUDDY_UID, memberUserIds: BUDDY_UID, claimRule: 1, recycleDays: 7, expireDays: 30, dailyClaimLimit: 10, holdLimit: 200 }) });""", """async function poolCreate() { try { const deptId = await freeDeptId(); const name = 'e2d-演示池-' + TS; // 部门成员须与负责人/协作人不同人:同用户会被「负责人>协作人>部门成员」去重优先级吞并(票 10 设计语义) const up = await api('/api/system/users/page', { method: 'POST', body: qs({ current: 1, size: 20 }) }); POOL.memberUid = (up.content || []).map(x => String(x.id)).find(id => id !== ADMIN_UID && id !== BUDDY_UID); if (!POOL.memberUid) throw new Error('找不到第三个用户做部门成员'); await api('/api/rule/pool/saveOrUpdate', { method: 'POST', body: qs({ poolName: name, deptId: String(deptId), ownerUserId: ADMIN_UID, collaboratorUserIds: BUDDY_UID, memberUserIds: POOL.memberUid, claimRule: 1, recycleDays: 7, expireDays: 30, dailyClaimLimit: 10, holdLimit: 200 }) });""") rep("const POOL = { demoId: null, importName: 'e2d-导入池-' + TS };", "const POOL = { demoId: null, memberUid: null, importName: 'e2d-导入池-' + TS };") rep(""" const ownerOk = String(d.ownerUserId) === ADMIN_UID; const collOk = (d.collaboratorUserIds || []).map(String).includes(BUDDY_UID); const memOk = (d.memberUserIds || []).map(String).includes(BUDDY_UID); if (ownerOk && collOk && memOk) ok(4, '三类成员回填一致:负责人=' + esc(d.ownerUserName || d.ownerUserId) + ',协作人=' + (d.collaboratorUserIds || []).length + ' 人,部门成员=' + (d.memberUserIds || []).length + ' 人'); else fail(4, new Error('成员回填不符: owner=' + d.ownerUserId + ' collab=' + JSON.stringify(d.collaboratorUserIds) + ' member=' + JSON.stringify(d.memberUserIds)));""", """ const ownerOk = String(d.ownerUserId) === ADMIN_UID; const collOk = (d.collaboratorUserIds || []).map(String).includes(BUDDY_UID); const memOk = (d.memberUserIds || []).map(String).includes(String(POOL.memberUid)); if (ownerOk && collOk && memOk) ok(4, '三类成员回填一致:负责人=' + esc(d.ownerUserName || d.ownerUserId) + ',协作人=' + (d.collaboratorUserIds || []).length + ' 人,部门成员=' + (d.memberUserIds || []).length + ' 人(uid=' + esc(POOL.memberUid) + ')'); else fail(4, new Error('成员回填不符: owner=' + d.ownerUserId + ' collab=' + JSON.stringify(d.collaboratorUserIds) + ' member=' + JSON.stringify(d.memberUserIds) + ' expectMember=' + POOL.memberUid));""") open(P, 'w', encoding='utf-8').write(t) print('ALL REPLACEMENTS OK, new len =', len(t))