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.
181 lines
10 KiB
181 lines
10 KiB
# -*- 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))
|
|
|