# -*- coding: utf-8 -*- """票 13:demo/index.html 六处修复(还愿断言/BUTTON必填/禁删靶子/refCount/池部门避让/BatchResult字段)""" 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[:60] t = t.replace(old, new, 1) # 1. trExec: 移交前快照 BUDDY 名下基数 rep("""async function trExec() { try { const d = await api('/api/system/users/transfer?userId=' + ADMIN_UID + '&receiverId=' + BUDDY_UID, { method: 'POST' });""", """async function trExec() { try { TR.buddyBase = await api('/api/system/users/transfer-preview?id=' + BUDDY_UID); const d = await api('/api/system/users/transfer?userId=' + ADMIN_UID + '&receiverId=' + BUDDY_UID, { method: 'POST' });""") # 2. trBack: 断言 BUDDY 名下净零(回落到移交前基数),admin 各域 >= 原快照 rep("""async function trBack() { try { const d = await api('/api/system/users/transfer?userId=' + BUDDY_UID + '&receiverId=' + ADMIN_UID, { method: 'POST' }); const pre = await api('/api/system/users/transfer-preview?id=' + ADMIN_UID); const base = TR.baseCounts || []; const restored = base.length && base.every(b => { const now = (pre || []).find(x => x.domain === b.domain); return now && Number(now.count) === Number(b.count); }); if (restored) ok(1, '返还成功:admin 各域计数已还原(' + base.map(x => x.domainName + '=' + x.count).join(',') + ')'); else fail(1, new Error('返还后计数不一致:期望 ' + JSON.stringify(base) + ',实际 ' + JSON.stringify(pre))); } catch (e) { fail(1, e); } }""", """async function trBack() { try { const d = await api('/api/system/users/transfer?userId=' + BUDDY_UID + '&receiverId=' + ADMIN_UID, { method: 'POST' }); // BUDDY 净零:名下计数回落到移交前基数(BUDDY 自有资产不在移交断言内逐域等值卡死) const preB = await api('/api/system/users/transfer-preview?id=' + BUDDY_UID); const baseB = TR.buddyBase || []; const buddyNetZero = baseB.length && baseB.every(b => { const now = (preB || []).find(x => x.domain === b.domain); return now && Number(now.count) === Number(b.count); }); const preA = await api('/api/system/users/transfer-preview?id=' + ADMIN_UID); const baseA = TR.baseCounts || []; const adminBack = baseA.length && baseA.every(b => { const now = (preA || []).find(x => x.domain === b.domain); return now && Number(now.count) >= Number(b.count); }); if (buddyNetZero && adminBack) ok(1, '返还成功:BUDDY 名下回落至移交前(' + baseB.map(x => x.domainName + '=' + x.count).join(',') + ');admin 各域 ≥ 原快照'); else fail(1, new Error('返还断言不符:buddyBase=' + JSON.stringify(baseB) + ' buddyNow=' + JSON.stringify(preB) + ' adminNow=' + JSON.stringify(preA))); } catch (e) { fail(1, e); } }""") # 3. resPrepare: BUTTON 必填 denyBehavior/apiUrl + 扫描被引用叶子作禁删靶子 rep(""" const d = await api('/api/resources/saveOrUpdate', { method: 'POST', body: qs({ parentId: RES.parentMenuId, type: 'BUTTON', name: 'e2d-演示权限点-' + TS, perms: 'e2d:demo:' + TS, sort: 9999, status: 'enabled', description: 'a7-parity e2e' }) }); RES.demoId = d && d.id; ok(1, '演示权限点就绪:id=' + esc(RES.demoId) + '(父 menu=' + esc(RES.parentMenuId) + ',商机管理目录=' + esc(RES.bizCatalogId) + ')');""", """ const d = await api('/api/resources/saveOrUpdate', { method: 'POST', body: qs({ parentId: RES.parentMenuId, type: 'BUTTON', name: 'e2d-演示权限点-' + TS, perms: 'e2d:demo:' + TS, denyBehavior: 'HIDE', apiUrl: '/api/e2d/demo:' + TS, sort: 9999, status: 'enabled', description: 'a7-parity e2e' }) }); 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; } } ok(1, '演示权限点就绪:id=' + esc(RES.demoId) + '(父 menu=' + esc(RES.parentMenuId) + ',商机管理目录=' + esc(RES.bizCatalogId) + ');禁删靶子(被引用 button 叶子)=' + esc(RES.delTargetId == null ? '未找到' : RES.delTargetId) + ',扫描 button 共 ' + buttons.length + ' 个');""") # 4. resDelBlocked: 用被引用叶子靶子 rep("""async function resDelBlocked() { try { if (!RES.bizCatalogId) throw new Error('先执行步骤0'); await api('/api/resources/delete?id=' + RES.bizCatalogId, { 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.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); } }""") # 5. dDraftPublish: refCount 数字化比较(Long 出参序列化为字符串 "0") rep("if (Number(det.status) === 2 && rc === 0) ok(3, 'D1 发布成功", "if (Number(det.status) === 2 && Number(rc) === 0) ok(3, 'D1 发布成功") # 6. 双 V 文案(versionNo 自带 V 前缀) rep("行,当前 V' + esc(cur.versionNo)", "行,当前 ' + esc(cur.versionNo)") rep("',versions V' + esc(v1.versionNo)", "',versions ' + esc(v1.versionNo)") # 7. poolCreate: 空闲部门避让(每部门仅一池 64003)+ pool/page 无名称过滤参数(size 200 全量找同名) rep("""async function poolCreate() { try { const deptId = await firstDeptId(); 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 }) }); const pg = await api('/api/rule/pool/page', { method: 'POST', body: qs({ current: 1, size: 50, poolName: name }) }); const row = (pg.content || []).find(x => x.poolName === name); if (!row) throw new Error('演示池创建后查不到'); POOL.demoId = row.id; ok(4, '演示池就绪:id=' + esc(POOL.demoId) + ',名称=' + esc(name) + '(三类成员已提交)'); } catch (e) { fail(4, e); } }""", """async function freeDeptId() { // 树序第一个未被公海池占用的部门:每部门仅一池(64003) const used = new Set(); const pg = await api('/api/rule/pool/page', { method: 'POST', body: qs({ current: 1, size: 200 }) }); (pg.content || []).forEach(r => used.add(String(r.deptId))); const tree = await api('/api/system/depts/tree'); let hit = null; const walk = ns => { for (const n of (ns || [])) { if (!hit && !used.has(String(n.id))) hit = n.id; walk(n.children); } }; walk(tree); if (!hit) throw new Error('无空闲部门可建池'); return hit; } 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 }) }); const pg = await api('/api/rule/pool/page', { method: 'POST', body: qs({ current: 1, size: 200 }) }); const row = (pg.content || []).find(x => x.poolName === name); if (!row) throw new Error('演示池创建后查不到'); POOL.demoId = row.id; ok(4, '演示池就绪:id=' + esc(POOL.demoId) + ',名称=' + esc(name) + ',部门=' + esc(deptId) + '(三类成员已提交)'); } catch (e) { fail(4, e); } }""") # 8. poolPage: 无名称过滤,全量取回按名断言 rep(""" const pg = await api('/api/rule/pool/page', { method: 'POST', body: qs({ current: 1, size: 100, poolName: 'e2d-' }) }); const names = (pg.content || []).map(x => x.poolName);""", """ const pg = await api('/api/rule/pool/page', { method: 'POST', body: qs({ current: 1, size: 200 }) }); const names = (pg.content || []).map(x => x.poolName);""") # 9. poolCleanup: 全量取回按名删 rep(""" const pg = await api('/api/rule/pool/page', { method: 'POST', body: qs({ current: 1, size: 100, poolName: 'e2d-' }) });""", """ const pg = await api('/api/rule/pool/page', { method: 'POST', body: qs({ current: 1, size: 200 }) });""") # 10. poolImport: BatchResult 字段名 successCount/failCount rep(""" const d = await api('/api/rule/pool/import', { method: 'POST', body: fd }); const total = d && (d.total != null ? d.total : (d.successCount != null ? d.successCount : '?')); const fails = d && d.failItems ? d.failItems.length : 0; log(4, 'import BatchResult → ' + JSON.stringify(d).slice(0, 200)); if (Number(total) >= 1 && fails === 0) ok(4, '导入成功:BatchResult total/success=' + total + ',失败=' + fails); else fail(4, new Error('导入结果异常: ' + JSON.stringify(d).slice(0, 200)));""", """ const d = await api('/api/rule/pool/import', { method: 'POST', body: fd }); const succ = d && d.successCount != null ? Number(d.successCount) : -1; const failCnt = d && d.failCount != null ? Number(d.failCount) : -1; log(4, 'import BatchResult → ' + JSON.stringify(d).slice(0, 200)); if (succ >= 1 && failCnt === 0) ok(4, '导入成功:BatchResult total=' + esc(d.total) + ',success=' + succ + ',fail=' + failCnt); else fail(4, new Error('导入结果异常: ' + JSON.stringify(d).slice(0, 200)));""") open(P, 'w', encoding='utf-8').write(t) print('ALL REPLACEMENTS OK, new len =', len(t))