You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

149 lines
8.5 KiB

# -*- 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))