@ -0,0 +1,78 @@ |
|||
# A7 后台管理验收包(a7-acceptance) |
|||
|
|||
- **生成**:2026-09-09(一期 [a7-parity](../a7-parity/demo/index.html) 12 tab + 二期 [a7-heavy](../a7-heavy/demo/index.html) 3 tab 合并;合并脚本 [_merge_demo.py](_merge_demo.py) 可重放) |
|||
- **目标环境**:测试服务 `https://ai.itc.vip/crm-api`(**demo 默认基址已指向,双击即用**;如需换环境,访问一次 `index.html?api=<新基址>` 即被记住) |
|||
|
|||
## 交付物清单 |
|||
|
|||
| 文件 | 用途 | |
|||
|---|---| |
|||
| [demo/index.html](demo/index.html) | **15 tab 统一验收 demo**(双击即用,默认管理员登录入口) | |
|||
| [验收步骤清单.md](验收步骤清单.md) | 逐 tab 逐步操作与预期说明,**结果列留白**待验收人填写 | |
|||
| [验收结果汇总.md](验收结果汇总.md) | 验收结果登记表(结构化缺陷块,**可整文件回喂 AI 定位修复**) | |
|||
| [issues/_TEMPLATE.md](issues/_TEMPLATE.md) | 单缺陷工单模板(本地 issue tracker 惯例,一缺陷一文件) | |
|||
| parity-smoke-result.json / heavy-smoke-result.json | 冒烟自测证据(58 步 + 22 步) | |
|||
| shots-parity/ · shots-heavy-run/ | 冒烟截图(各 10 张 / 9 张) | |
|||
| _merge_demo.py · _check_demo.py · _gen_checklist.py | 再生成 / 静态校验 / 清单再生成工具 | |
|||
|
|||
## 新人上手(4 步) |
|||
|
|||
1. **打开**:双击 `demo/index.html`,页头「API:」应显示 `https://ai.itc.vip/crm-api`。 |
|||
2. **登录**:点「uid 登录」→ 应显示「已登录:罗伟健 (739564171091247104)」。 |
|||
3. **验收**:按《[验收步骤清单.md](验收步骤清单.md)》逐 tab 从上到下点每个步骤按钮——输出框**绿色 = PASS**,**红色(失败 + code=)= 候选缺陷**;每步结果填进清单结果列。 |
|||
4. **记缺陷**:FAIL 的按《[验收结果汇总.md](验收结果汇总.md)》格式登记(界面红框原文、执行日志面板里的请求 URL/参数直接复制);每 tab 末尾的「清理」步骤**必须执行**,不留演示数据。 |
|||
|
|||
## 覆盖范围(15 tab = A7 全分区) |
|||
|
|||
| tab | 原型分区 | 覆盖 | |
|||
|---|---|---| |
|||
| 用户管理 user | A7-1 | 用户列表 / 统计三时间戳 / 移交预览·自移交 61019·执行清零·返还 | |
|||
| 同步任务 sync | A7-1-2(二期票01) | 任务化契约 SYNC-MMdd / 异步终态 / 61023 并发占位 / 分页倒序 / 详情五块 / 设置读写+61022 / lastSyncTime 切表 | |
|||
| 角色管理 role | A7-2 | stats 四计数 / 批量增量分配 / copy 停用草稿 / enable 61021 / 内置角色保护 | |
|||
| 权限点管理 res | A7-2 | references 引用清单 / 停启 roundtrip / 禁删两段 61009+61020 | |
|||
| 方案推送规则 push | A7-3 | D-01 无步骤④发布 / 4 步骤回显 / 64029 / 64030 / copy / 64027 | |
|||
| 项目查重规则 dedup | A7-3 | 发布+referenceCount / 64031 两路 / 引用计数入账 / 64030 / 64033 | |
|||
| 项目规则 projrule | A7-3 | current 5 区块聚合 / 分区保存回显 / 64036 比例校验 | |
|||
| 线索池 pool | A7-3 线索规则 | 三类异人成员建池回填 / 导出导入 BatchResult / 列表 / 清理 | |
|||
| 公海绑定数 poolrule | A7-3-4(二期票03) | 部门专用规则发布 / boundOppCount 增量 base+n / 默认规则兜底 / 清理 | |
|||
| 全局日志 glog | A7-4(二期票02 · crm-log) | AOP 采集 / 操作·登录·批量三日志 / 结果文件下载 ZIP / 打包下载 / 40401 形态 / BUDDY 权限拒绝 | |
|||
| 字典种子验证 seed | A7-5 | opp_stage 06/07 + opportunity_status 4 值 | |
|||
| 分组管理 dgroup | A7-5(v3 契约) | 新建/编辑/启停/删除/设默认值(仅二级项) | |
|||
| 字典项管理 ditem | A7-5(v3 契约) | 无键值自动编码 / 组内名称唯一 63007 / 复合子项 / 63013 默认项禁停 | |
|||
| 两级树 dtree | A7-5 | 一级列表 + 行内二级展开 | |
|||
| 引用计数联测 dref | A7-5 | 客户引用 +1 / 换引用迁移 / 引用项禁删 63009 / 归档保留计数 | |
|||
|
|||
## 登录与账号 |
|||
|
|||
- **管理员**:罗伟健 `739564171091247104`(页头默认选中) |
|||
- **非管理员**:曾偲青(BUDDY) `744842318024015872`——全局日志 tab 步骤 9 用它验证权限拒绝口径 |
|||
- 登录通道 = `GET /api/auth/debug/token?userId=`(verify 环境调试通道);输入框可粘贴**任意测试环境 userId** 直登 |
|||
- 演示数据一律 `e2d-` 前缀可辨;操作/登录/批量日志行与同步任务行为审计数据,留存不删 |
|||
|
|||
## 已知边界与争议留白(🟡 不报缺陷) |
|||
|
|||
- 数据共享 3 页(A7-2-3 backup):等原型正式化,未纳入盘点。 |
|||
- 同步任务两态容忍:测试环境钉钉凭据不可达 → 任务落 all_failed(或长时间 running)为正常形态;61023 并发占位同理。 |
|||
- lastSyncTime 口径 = 同步任务表最新成功行 endTime,无成功行为空。 |
|||
- 职务下拉值域来源、「参与商机数」列、池规则冗余列、幻影挂账 3 端点、项目规则「阶段提醒」区块:**争议留白**,见清单尾部待确认清单。 |
|||
- 全局日志采集 = 写侧(POST)先行不采查询 GET;登录日志仅「退出」钩子采集(debug/token 不走登录采集点)。 |
|||
|
|||
## 冒烟自测(可选,验收人自证环境可用) |
|||
|
|||
```bash |
|||
# python 用仓库自带 venv;两个脚本均已支持 E2D_* 环境变量 |
|||
E2D_API=https://ai.itc.vip/crm-api \ |
|||
E2D_DEMO='E:\code\crm-backend-matt\.scratch\a7-acceptance\demo\index.html' \ |
|||
E2D_SHOTS='E:\code\crm-backend-matt\.scratch\a7-acceptance\shots-parity' \ |
|||
E2D_RESULT='E:\code\crm-backend-matt\.scratch\a7-acceptance\parity-smoke-result.json' \ |
|||
.scratch/customer-e2e/.venv10/Scripts/python.exe .scratch/a7-parity/demo_smoke.py # 一期 58 步 |
|||
|
|||
E2D_API=... E2D_DEMO=... E2D_SHOTS='...shots-heavy-run' E2D_RESULT='...heavy-smoke-result.json' \ |
|||
.scratch/customer-e2e/.venv10/Scripts/python.exe .scratch/a7-heavy/demo_smoke.py # 二期 22 步 |
|||
``` |
|||
|
|||
**最近结论(2026-09-09)**:一期 58/58、二期 22/22 **ALL_PASS**(对测试服务),console/page error 双零(BUDDY 403 拒绝豁免除外),网络层零 PUT/DELETE、零路径变量形态。 |
|||
|
|||
## 给负责人的回喂流程 |
|||
|
|||
新人填完《验收结果汇总.md》→ **整个文件直接交给 AI**(附本目录与后端仓 `E:\code\crm-backend-matt` 路径)→ AI 按缺陷块逐条定位修复;争议留白项等负责人确认后再改版。 |
|||
@ -0,0 +1,35 @@ |
|||
# -*- coding: utf-8 -*- |
|||
"""统一 demo 静态校验:重复 const / onclick 函数覆盖 / 重复 function 定义。""" |
|||
import io, re, subprocess, sys, collections |
|||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') |
|||
|
|||
P = r'E:\code\crm-backend-matt\.scratch\a7-acceptance\demo\index.html' |
|||
t = open(P, encoding='utf-8').read() |
|||
js = re.search(r'<script>(.*)</script>', t, re.S).group(1) |
|||
|
|||
# 1. node --check 语法校验 |
|||
r = subprocess.run(['node', '--check', '--input-type=commonjs', '-'], input=js.encode('utf-8'), |
|||
capture_output=True) |
|||
print('node --check:', 'OK' if r.returncode == 0 else 'FAIL') |
|||
if r.returncode != 0: |
|||
print(r.stderr.decode('utf-8', 'replace')[:1500]) |
|||
|
|||
# 2. 重复 const 声明 |
|||
consts = re.findall(r'^const\s+(\w+)', js, re.M) |
|||
dup = [k for k, c in collections.Counter(consts).items() if c > 1] |
|||
print('重复 const:', dup if dup else '无') |
|||
|
|||
# 3. onclick 引用函数是否都定义 |
|||
called = set(re.findall(r'onclick="(\w+)\(', t)) |
|||
defined = set(re.findall(r'(?:async\s+)?function\s+(\w+)\(', js)) | set(consts) |
|||
missing = sorted(c for c in called if c not in defined) |
|||
print('onclick 引用数:', len(called), '| 未定义:', missing if missing else '无') |
|||
|
|||
# 4. 重复 function 声明(同名) |
|||
fns = re.findall(r'^(?:async\s+)?function\s+(\w+)\(', js, re.M) |
|||
dupfn = [k for k, c in collections.Counter(fns).items() if c > 1] |
|||
print('重复 function:', dupfn if dupfn else '无') |
|||
|
|||
# 5. 关键锚点 |
|||
for a in ['SY_STATUS_TXT', 'STATUS_TXT', 'syncPage()', 'infoBox', 'waitFor', 'remintToken']: |
|||
print(f' {a}:', js.count(a)) |
|||
@ -0,0 +1,91 @@ |
|||
# -*- coding: utf-8 -*- |
|||
"""从统一 demo 生成《验收步骤清单.md》:逐 tab 提取联测步骤(按钮 + 预期说明),结果列留白。""" |
|||
import io, re, sys, html, pathlib |
|||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') |
|||
|
|||
ROOT = pathlib.Path(__file__).resolve().parent |
|||
t = (ROOT / 'demo' / 'index.html').read_text(encoding='utf-8') |
|||
|
|||
TAB_META = { |
|||
'user': ('A7-1 用户管理', '用户列表 / 统计出参 / 移交全链路 / 时间戳'), |
|||
'sync': ('A7-1-2 同步任务(二期票01)', '任务化契约 / 分页倒序 / 详情五块 / 设置读写与边界 / lastSyncTime 切表'), |
|||
'role': ('A7-2 角色管理', '统计四计数 / 批量增量分配 / copy / 启停校验 / 内置保护'), |
|||
'res': ('A7-2 权限点管理', '引用清单 / 停启 roundtrip / 禁删两段'), |
|||
'push': ('A7-3 方案推送规则', '状态机全链(草稿/发布/64029/64030/copy/64027)'), |
|||
'dedup': ('A7-3 项目查重规则', '发布与引用计数 / 64031 两路 / 64030 / 64033'), |
|||
'projrule': ('A7-3 项目规则', 'current 5 区块聚合 / 分区保存 / 64036 比例校验'), |
|||
'pool': ('A7-3 线索规则·线索池', '三类异人成员 / 导出导入 / 列表 / 清理'), |
|||
'poolrule': ('A7-3-4 公海规则「绑定商机(数量)」(二期票03)', '部门专用规则发布 / boundOppCount 增量 / 默认兜底 / 清理'), |
|||
'glog': ('A7-4 日志管理(二期票02 · crm-log)', 'AOP 采集 / 操作·登录·批量日志 / 结果文件 / 权限口径'), |
|||
'seed': ('A7-5 字典种子验证', 'opp_stage 06/07 + opportunity_status 4 值'), |
|||
'dgroup': ('A7-5 数据字典·分组管理(v3 契约)', '新建/编辑/启停/删除/设默认值(仅二级项)'), |
|||
'ditem': ('A7-5 数据字典·字典项管理(v3 契约)', '无键值自动编码 / 组内名称唯一 63007 / 复合子项 / 63013 禁停'), |
|||
'dtree': ('A7-5 数据字典·两级树', '一级列表 + 行内二级展开'), |
|||
'dref': ('A7-5 数据字典·引用计数联测', '客户引用 +1 / 换引用迁移 / 引用项禁删 63009 / 归档保留计数'), |
|||
} |
|||
|
|||
def strip(h): |
|||
h = re.sub(r'<[^>]+>', ' ', h) |
|||
return re.sub(r'\s+', ' ', html.unescape(h)).strip() |
|||
|
|||
out = [] |
|||
out.append('# A7 后台管理验收步骤清单(a7-acceptance · 15 页全分区)\n') |
|||
out.append('- **被测对象**:`demo/index.html`(15 tab 统一版)× 测试环境 `https://ai.itc.vip/crm-api`') |
|||
out.append('- **使用方法**:双击打开 demo → 页头「uid 登录」(默认管理员罗伟健)→ 按下表逐 tab 从上到下点每个步骤按钮 →') |
|||
out.append(' 输出框**绿色(✅/提示)= PASS**,**红色(失败 + code=)= 候选缺陷** → 结果列填 PASS / FAIL,FAIL 的按《验收结果汇总.md》格式记缺陷。') |
|||
out.append('- **纪律**:数字类断言(条数/计数)随库内数据浮动,以**相对变化**为准;每 tab 最后的「清理」步骤务必执行,不留演示数据。\n') |
|||
|
|||
for tab, label in re.findall(r'<button data-tab="([^"]+)"[^>]*>([^<]+)</button>', t): |
|||
sec_m = re.search(r'<section id="tab-' + tab + r'"[^>]*>(.*?)</section>', t, re.S) |
|||
if not sec_m: |
|||
continue |
|||
sec = sec_m.group(1) |
|||
meta = TAB_META.get(tab, ('', '')) |
|||
out.append(f'\n---\n\n## {label}({tab})· {meta[0]}\n') |
|||
if meta[1]: |
|||
out.append(f'> 覆盖:{meta[1]}\n') |
|||
steps = re.findall(r'<div class="step"><b>([^<]*)</b>(.*?)</div>', sec, re.S) |
|||
if steps: |
|||
out.append('| 步骤 | 操作 | 说明 / 预期 | 结果(PASS/FAIL) | 备注 |') |
|||
out.append('|---|---|---|---|---|') |
|||
for num, body in steps: |
|||
btns = re.findall(r'<button[^>]*>([^<]+)</button>', body) |
|||
hints = re.findall(r'<span class="hint">(.*?)</span>', body, re.S) |
|||
desc = ';'.join(strip(h) for h in hints) or '—' |
|||
out.append(f'| {strip(num)} | {" / ".join(strip(b) for b in btns) or "—"} | {desc} | | |') |
|||
else: |
|||
btns = [] |
|||
for b in re.findall(r'<button[^>]*>([^<]+)</button>', sec): |
|||
x = strip(b) |
|||
if x and x not in btns: |
|||
btns.append(x) |
|||
out.append('本页为主界面操作(无联测步骤面板),对照原型逐控件核对:\n') |
|||
out.append('可操作入口:' + '、'.join(btns)) |
|||
out.append('\n| 操作 | 预期 | 结果(PASS/FAIL) | 备注 |') |
|||
out.append('|---|---|---|---|') |
|||
out.append('| 逐控件操作并观察列表/弹窗反馈 | 与原型 A7 对应页一致(弹窗字段、错误码文案、列表列头) | | |') |
|||
|
|||
out.append(''' |
|||
--- |
|||
|
|||
## 争议留白(待产品/负责人确认,本轮不作为缺陷) |
|||
|
|||
- [ ] 数据共享 3 页(A7-2-3 backup 草稿箱):等原型正式化,未纳入盘点与验收。 |
|||
- [ ] 职务下拉值域来源(OA title 去重 vs 字典分组):挂账待拍板。 |
|||
- [ ] 角色管理「处理使用人员」弹窗「当前参与商机数」列:计数口径未拍板。 |
|||
- [ ] OpportunityPoolRule 列表「所属战队」等冗余列:随 UI 确认后补。 |
|||
- [ ] 幻影挂账 3 端点(POST /depts/save、/depts/delete、/users/admin-role):原型无入口,保留运维用途。 |
|||
- [ ] 项目规则「阶段提醒」区块:原型自注「待补充」。 |
|||
- [ ] 全局日志采集覆盖面 = 写侧(POST)先行,不采「查询」GET;登录日志仅采「退出」钩子(debug/token 不走登录采集点)。 |
|||
|
|||
## 已知形态说明(不算缺陷) |
|||
|
|||
- 同步任务两态容忍:测试环境钉钉凭据不可达时,任务以 all_failed 终态落行(或长时间 running)均为正常形态。 |
|||
- lastSyncTime 口径 = 同步任务表最新成功行 endTime;无成功行时为空。 |
|||
- 操作/登录/批量日志行与同步任务行 = 审计数据,留存不删;其余演示数据一律 e2d- 前缀 + 步骤面板末步清理。 |
|||
''') |
|||
|
|||
p = ROOT / '验收步骤清单.md' |
|||
p.write_text('\n'.join(out), encoding='utf-8') |
|||
print('OK ->', p) |
|||
print('tabs:', len(re.findall(r'<button data-tab=', t)), '| steps rows:', sum(len(re.findall(r'<div class="step">', m)) for m in re.findall(r'<section id="tab-[^"]*"[^>]*>(.*?)</section>', t, re.S))) |
|||
@ -0,0 +1,112 @@ |
|||
# -*- coding: utf-8 -*- |
|||
"""合并 A7 一期(12 tab) + 二期(3 tab) demo 为统一验收 demo(15 tab)。 |
|||
用法: python _merge_demo.py |
|||
输出: demo/index.html(.scratch/a7-acceptance/demo/) |
|||
""" |
|||
import io, re, pathlib, sys |
|||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') |
|||
|
|||
ROOT = pathlib.Path(__file__).resolve().parent |
|||
PAR = pathlib.Path(r'E:\code\crm-backend-matt\.scratch\a7-parity\demo\index.html') |
|||
HVY = pathlib.Path(r'E:\code\crm-backend-matt\.scratch\a7-heavy\demo\index.html') |
|||
OUT_DIR = ROOT / 'demo' |
|||
OUT = OUT_DIR / 'index.html' |
|||
|
|||
par = PAR.read_text(encoding='utf-8') |
|||
hvy = HVY.read_text(encoding='utf-8') |
|||
assert par.count('<script>') == 1 and hvy.count('<script>') == 1 |
|||
|
|||
# ---------- 1. 标题与 h1 ---------- |
|||
par = par.replace('<title>A7 后台管理 × 数据字典 E2E Demo(a7-parity)</title>', |
|||
'<title>A7 后台管理 E2E 验收 Demo(a7-acceptance · 15 页全分区)</title>') |
|||
par = par.replace('<h1>A7 后台管理 × 数据字典 E2E Demo</h1>', |
|||
'<h1>A7 后台管理 E2E 验收 Demo</h1>') |
|||
|
|||
# ---------- 2. nav 按钮(按原型分区顺序插入) ---------- |
|||
# A7-1 用户管理 后插「同步任务」 |
|||
anchor = '<button data-tab="user" class="on" onclick="switchTab(\'user\')">用户管理</button>' |
|||
assert par.count(anchor) == 1 |
|||
par = par.replace(anchor, anchor + '\n <button data-tab="sync" onclick="switchTab(\'sync\')">同步任务</button>') |
|||
# A7-3 线索池 后插「公海绑定数」+「全局日志」(A7-4 日志) |
|||
anchor2 = '<button data-tab="pool" onclick="switchTab(\'pool\')">线索池</button>' |
|||
assert par.count(anchor2) == 1 |
|||
par = par.replace(anchor2, anchor2 + |
|||
'\n <button data-tab="poolrule" onclick="switchTab(\'poolrule\')">公海绑定数</button>' + |
|||
'\n <button data-tab="glog" onclick="switchTab(\'glog\')">全局日志</button>') |
|||
|
|||
# ---------- 3. 二期三个 section 插入 </main> 前 ---------- |
|||
hvy_sec_start = hvy.index('<!-- ========== Tab 同步任务') |
|||
hvy_sec_end = hvy.index('</main>') |
|||
hvy_sections = hvy[hvy_sec_start:hvy_sec_end].rstrip() + '\n' |
|||
assert 'id="tab-sync"' in hvy_sections and 'id="tab-glog"' in hvy_sections and 'id="tab-poolrule"' in hvy_sections |
|||
par = par.replace('</main>', hvy_sections + '</main>', 1) |
|||
|
|||
# ---------- 4. JS 合并 ---------- |
|||
# 4a. 一期 loginByUid 登录后补 syncPage()(同步任务页登录即加载) |
|||
old_login_tail = 'S_G.page = 1; loadGroups(); loadGroupOptions(); loadUsers();' |
|||
assert par.count(old_login_tail) == 1 |
|||
par = par.replace(old_login_tail, old_login_tail + ' syncPage();') |
|||
|
|||
# 4b. 一期 switchTab 加 sync 钩子 |
|||
old_sw = " if (t === 'ditem') loadGroupOptions(); // 分组可能刚在分组页新建/启停,进字典项页刷新下拉\n" \ |
|||
" if (t === 'dtree' && !$('tGroup').options.length) loadGroupOptions().then(loadTree);\n}" |
|||
assert par.count(old_sw) == 1 |
|||
new_sw = old_sw.replace('}', " if (t === 'sync' && ME()) syncPage(); // 二期:同步任务页登录后进页即加载\n}", 1) |
|||
par = par.replace(old_sw, new_sw) |
|||
|
|||
# 4c. 一期初始化补 syncPage() |
|||
old_init = "if (ME()) { loadUsers(); loadGroups().then(loadGroupOptions); }" |
|||
assert par.count(old_init) == 1 |
|||
par = par.replace(old_init, "if (ME()) { loadUsers(); loadGroups().then(loadGroupOptions); syncPage(); }") |
|||
|
|||
# 4d. 二期 JS 移植窗口:fetchBytes 注释起(共享工具已由一期底座提供),到「初始化」注释止 |
|||
g_start = hvy.index('/* 二进制下载助手(对称一期 poolExport):带 token fetch → bytes */') |
|||
g_end = hvy.index('/* ================= 初始化 ================= */') |
|||
graft = hvy[g_start:g_end] |
|||
|
|||
# 排除与一期底座冲突/需让位的函数(一期版本必须胜出) |
|||
def cut_fn(src, name): |
|||
"""删除 graft 中名为 name 的函数声明块(含前置注释行可选项不处理)。""" |
|||
pat = re.compile(r'(async )?function ' + name + r'\(.*?\n\}', re.S) |
|||
m = pat.search(src) |
|||
assert m, 'graft 中未找到 ' + name |
|||
return src[:m.start()] + src[m.end():] |
|||
|
|||
for name in ('loginByUid', 'switchTab', 'paintWho'): |
|||
graft = cut_fn(graft, name) |
|||
|
|||
# 与一期底座完全同源的输出盒函数:精确文本删除(单行函数,勿用正则 cut_fn——会连吞后续多行函数) |
|||
DUP_TEXTS = [ |
|||
"function outBox(box, html) { document.getElementById(box).innerHTML = html; }\n", |
|||
"function okBox(box, msg) { outBox(box, '<div class=\"okbox\">' + msg + '</div>'); }\n", |
|||
"function failBox(box, e) { outBox(box, errHtml(e)); }\n", |
|||
"function logLine(box, line) {\n const b = document.getElementById(box);\n" |
|||
" b.textContent += '\\n[' + new Date().toLocaleTimeString() + '] ' + line;\n" |
|||
" b.scrollTop = b.scrollHeight;\n}\n", |
|||
] |
|||
for t in DUP_TEXTS: |
|||
assert graft.count(t) == 1, 'dup 文本未找到: ' + t[:40] |
|||
graft = graft.replace(t, '') |
|||
|
|||
# STATUS_TXT 与一期字典映射冲突 → 二期同步状态映射改名 |
|||
assert 'const STATUS_TXT' in graft |
|||
graft = graft.replace('STATUS_TXT', 'SY_STATUS_TXT') |
|||
|
|||
# 插入位置:一期「初始化」注释之前 |
|||
ins = '/* ================= 初始化 ================= */' |
|||
assert par.count(ins) == 1 |
|||
par = par.replace(ins, '/* ================= 二期移植(a7-heavy 票 01-03:同步任务 / 全局日志 / 公海绑定数) ================= */\n' |
|||
+ graft.rstrip() + '\n\n' + ins, 1) |
|||
|
|||
# ---------- 4e. 双击默认基址 = 测试服务(带 ?api= 或 localStorage 记忆仍可覆盖) ---------- |
|||
old_fallback = "return location.protocol.startsWith('http') ? '' : 'http://localhost:8080';" |
|||
assert par.count(old_fallback) == 1 |
|||
par = par.replace(old_fallback, "return location.protocol.startsWith('http') ? '' : 'https://ai.itc.vip/crm-api';") |
|||
|
|||
# ---------- 5. 输出 ---------- |
|||
OUT_DIR.mkdir(parents=True, exist_ok=True) |
|||
OUT.write_text(par, encoding='utf-8') |
|||
print('OK ->', OUT) |
|||
print('total lines:', len(par.splitlines())) |
|||
print('tabs:', len(re.findall(r'<button data-tab=', par))) |
|||
print('sections:', len(re.findall(r'<section id="tab-', par))) |
|||
@ -0,0 +1,143 @@ |
|||
{ |
|||
"ts": "221152", |
|||
"console_errors": [ |
|||
"Failed to load resource: the server responded with a status of 403 ()", |
|||
"Failed to load resource: the server responded with a status of 403 ()", |
|||
"Failed to load resource: the server responded with a status of 403 ()" |
|||
], |
|||
"page_errors": [], |
|||
"exempt_403": [ |
|||
"Failed to load resource: the server responded with a status of 403 ()", |
|||
"Failed to load resource: the server responded with a status of 403 ()", |
|||
"Failed to load resource: the server responded with a status of 403 ()" |
|||
], |
|||
"real_console_errors": [], |
|||
"results": [ |
|||
{ |
|||
"step": "登录 debug/token + /auth/me", |
|||
"ok": true, |
|||
"note": "" |
|||
}, |
|||
{ |
|||
"step": "同步任务 launch(任务编号格式 + 61023 两态容忍)", |
|||
"ok": true, |
|||
"note": "任务化契约验证:taskId=753014778993049600,taskNo=SYNC-0909-02(SYNC-MMdd-序号 ✓) 异步终态:running(75s 未终态,任务行在位即可) " |
|||
}, |
|||
{ |
|||
"step": "同步记录分页 + 开始时间倒序", |
|||
"ok": true, |
|||
"note": "rows=2 desc=True first=2026-09-09 02:00:00" |
|||
}, |
|||
{ |
|||
"step": "任务详情五块结构(含 failDetails 失败明细)", |
|||
"ok": true, |
|||
"note": "详情五块结构在位:taskNo=SYNC-0909-02,status=同步中,进度=5%,失败明细 0 条" |
|||
}, |
|||
{ |
|||
"step": "同步设置回显(派生 plan/nextExecuteTime/lastModifierName)", |
|||
"ok": true, |
|||
"note": "设置回显:autoSyncEnabled=true,dailyTime=02:00,retryIntervalMinutes=10,retryMaxTimes=2,concurrency=1 派生:当前计划=每日 02:00(运行中),下次" |
|||
}, |
|||
{ |
|||
"step": "设置保存回显(interval=2 次数=3 并发=1)", |
|||
"ok": true, |
|||
"note": "合法设置保存并回显:interval=2,次数=3,并发=1(autoSyncEnabled/dailyTime 保留原值不污染调度)" |
|||
}, |
|||
{ |
|||
"step": "设置边界 61022 双路(间隔<1min / 次数<1 拒)", |
|||
"ok": true, |
|||
"note": "61022 边界校验两路生效:间隔<1min 拒(重试间隔不能小于 1 分钟);次数<1 拒(重试次数不能小于 1 次)" |
|||
}, |
|||
{ |
|||
"step": "设置还原(不污染调度原值)", |
|||
"ok": true, |
|||
"note": "设置已还原(interval=10,次数=2,并发=1,dailyTime=02:00)" |
|||
}, |
|||
{ |
|||
"step": "lastSyncTime 切表口径 + 下次执行时间推导", |
|||
"ok": true, |
|||
"note": "lastSyncTime 验证:与最新成功任务行 endTime 一致(2026-09-08 17:18:35)——切表生效 下次执行时间推导:autoSyncEnabled=true,nextExecuteTime=2026-09-10 02:00:00(推" |
|||
}, |
|||
{ |
|||
"step": "AOP 写端点采集素材(建/删演示角色)", |
|||
"ok": true, |
|||
"note": "写操作完成:建/删演示角色 e2d-日志演示角色-221152(等待采集落库后断言)" |
|||
}, |
|||
{ |
|||
"step": "操作日志分页(新增/删除行命中 + 时间范围逐字段)", |
|||
"ok": true, |
|||
"note": "AOP 采集行命中:新增行 content=/api/roles/saveOrUpdate,操作人=罗伟健,结果=成功 逐字段:今日范围 10 行 / 2000-01-01 范围 0 行(时间过滤生效)" |
|||
}, |
|||
{ |
|||
"step": "登录日志「退出」行(内联埋点 + token 重铸)", |
|||
"ok": true, |
|||
"note": "退出埋点命中:opType=退出,operatorName=罗伟健,status/result=成功,ip=116.7.105.4,opTime=2026-09-09 22:13:14 (token " |
|||
}, |
|||
{ |
|||
"step": "批量日志「导出」行(result+fileId+ZIP 魔数)", |
|||
"ok": true, |
|||
"note": "导出留档行命中:page=线索设置,result=成功,countDetail=成功导出 1 条公海池,fileId=753015119432122368 (导出字节 4047,ZIP 魔数 ✓)" |
|||
}, |
|||
{ |
|||
"step": "批量日志「导入」行(BatchResult 计数)", |
|||
"ok": true, |
|||
"note": "导入行命中:result=成功,countDetail=成功导入 1,导入失败 0,fileId=(无失败明细→无错误报告文件,合理) BatchResult: total=1,success=1,fail=0" |
|||
}, |
|||
{ |
|||
"step": "结果文件下载 /api/log/batch/file(ZIP 魔数)", |
|||
"ok": true, |
|||
"note": "结果文件下载成功:日志行 id=753015119490842624(fileId=753015119432122368),4047 字节,ZIP 魔数 PK ✓(90 天内有效)" |
|||
}, |
|||
{ |
|||
"step": "打包下载 /api/log/batch/download ids[](ZIP 魔数)", |
|||
"ok": true, |
|||
"note": "打包下载成功:ids=[753015119490842624, 753015121080483840],3506 字节,ZIP 魔数 PK ✓" |
|||
}, |
|||
{ |
|||
"step": "不存在文件 id 错误形态(40401/66001/404)", |
|||
"ok": true, |
|||
"note": "不存在文件 id 错误形态生效:code=40401,批量操作日志不存在: 999999999999 (查无日志行→40401/HTTP404;过期→66001;行存在但无文件→66002;90 天分" |
|||
}, |
|||
{ |
|||
"step": "BUDDY 三 page 权限拒绝(ADMIN 门禁)", |
|||
"ok": true, |
|||
"note": "非管理员权限口径生效(ADMIN 门禁): page=拒绝 ✓(HTTP 403) page=拒绝 ✓(HTTP 403) page=拒绝 ✓(HTTP 403)" |
|||
}, |
|||
{ |
|||
"step": "公海规则发布(部门专用)+ boundOppCount 基线", |
|||
"ok": true, |
|||
"note": "规则就绪:id=753015126608576512,name=e2d-绑定数规则-221152,scope=2(部门专用),deptIds=[744841292348915712] boundOppCount 基线 base=11571(本轮新建发布) 后续" |
|||
}, |
|||
{ |
|||
"step": "商机 A → boundOppCount=base+1(match 语义)", |
|||
"ok": true, |
|||
"note": "商机 A(id=753015130098237440)→ boundOppCount=11572(base+1 ✓,match 语义生效)" |
|||
}, |
|||
{ |
|||
"step": "商机 B → boundOppCount=base+2(动态统计非快照)", |
|||
"ok": true, |
|||
"note": "商机 B(id=753015131109064704)→ boundOppCount=11573(base+2 ✓,动态统计非快照)" |
|||
}, |
|||
{ |
|||
"step": "默认规则兜底口径(无默认规则降级提示)", |
|||
"ok": true, |
|||
"note": "当前无发布中默认规则 → 兜底口径断言降级为提示(口径已由 crm-rule 单测覆盖:默认计数=不命中发布中专用规则的商机数,排除集=发布中专用规则部门并集)" |
|||
}, |
|||
{ |
|||
"step": "清理(商机 close 正门 + 规则停用)", |
|||
"ok": true, |
|||
"note": "清理完成:关停演示商机 2/2(close 终态;已关闭仍计数,仅 opp_status=5 排除——增量口径跨轮安全);演示规则已停用(发布行留存可辨)" |
|||
}, |
|||
{ |
|||
"step": "网络层零旧契约形态", |
|||
"ok": true, |
|||
"note": "bad_method=[] old_style=[] (共84请求)" |
|||
}, |
|||
{ |
|||
"step": "console/page error 双零(预期 403 拒绝豁免)", |
|||
"ok": true, |
|||
"note": "raw_console=3 exempt_403=3 real=0 page_errors=0" |
|||
} |
|||
], |
|||
"all_pass": true |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
# BUG-<NN> <一句话现象> |
|||
|
|||
Status: needs-triage |
|||
Type: defect |
|||
|
|||
- **发现途径**:A7 验收 demo(a7-acceptance · 15 tab 统一版)× 测试环境 https://ai.itc.vip/crm-api |
|||
- **所属 tab / 原型分区**: |
|||
- **触发步骤**(tab + 步骤号 + 按钮名): |
|||
- **接口**:`METHOD /api/...` |
|||
- **请求参数**: |
|||
- **预期**: |
|||
- **实际**(红框原文含 code=): |
|||
- **复现方式**: |
|||
- **严重级**:P0 / P1 / P2 / P3 |
|||
- **证据**(截图/控制台报错): |
|||
|
|||
> 约定:一缺陷一文件,命名 `NN-<slug>.md`(NN 从 01 递增);本模板字段与《验收结果汇总.md》§2 缺陷块一致——汇总文件填完后,负责人把 FAIL 项逐条落到本目录成票。 |
|||
|
|||
## Comments |
|||
@ -0,0 +1,298 @@ |
|||
{ |
|||
"ts": "221451", |
|||
"console_errors": [], |
|||
"page_errors": [], |
|||
"results": [ |
|||
{ |
|||
"step": "登录 debug/token + /auth/me", |
|||
"ok": true, |
|||
"note": "" |
|||
}, |
|||
{ |
|||
"step": "字典分组列表预加载 dict/group/page(移植回归)", |
|||
"ok": true, |
|||
"note": "" |
|||
}, |
|||
{ |
|||
"step": "字典项列表预加载 dict/item/page(移植回归)", |
|||
"ok": true, |
|||
"note": "" |
|||
}, |
|||
{ |
|||
"step": "v3 建演示分组A", |
|||
"ok": true, |
|||
"note": "e2d_group_a_221451" |
|||
}, |
|||
{ |
|||
"step": "v3 弹窗结构(无键值/无默认勾选/子项编码列)", |
|||
"ok": true, |
|||
"note": "键值输入=True 默认勾选=True" |
|||
}, |
|||
{ |
|||
"step": "v3 新建项(无键值)编码自动生成", |
|||
"ok": true, |
|||
"note": "code=dict_c33ce3ff00d646b3bd054961f3e5362e" |
|||
}, |
|||
{ |
|||
"step": "v3 名称组内唯一 63007", |
|||
"ok": true, |
|||
"note": "失败: 字典名称[e2d-项1]在本分组内已存在 (code=63007)" |
|||
}, |
|||
{ |
|||
"step": "v3 复合编辑加子项(编码自动生成)", |
|||
"ok": true, |
|||
"note": "子项code=dict_65ce74e1083948a4b33c06a6e46cdaca" |
|||
}, |
|||
{ |
|||
"step": "v3 分组设默认(仅二级项可选 + 父名/子名回显)", |
|||
"ok": true, |
|||
"note": "两级提示=True cell=e2d-项1 / e2d-子项1" |
|||
}, |
|||
{ |
|||
"step": "v3 分组默认项禁停 63013(行停用路)", |
|||
"ok": true, |
|||
"note": "" |
|||
}, |
|||
{ |
|||
"step": "v3 演示分组清理", |
|||
"ok": true, |
|||
"note": "e2d_group_a_221451 left=0 [子项:0:success | 一级:0:success | 分组:0:success]" |
|||
}, |
|||
{ |
|||
"step": "用户分页 users/page", |
|||
"ok": true, |
|||
"note": "rows=10" |
|||
}, |
|||
{ |
|||
"step": "用户统计出参(三时间戳字段在位)", |
|||
"ok": true, |
|||
"note": "部门=814,在职=6157,离职=0,未分配=6150 lastSyncTime=2026-09-08 17:18:35,lastTransferTime=2026-09-09 " |
|||
}, |
|||
{ |
|||
"step": "移交预览 transfer-preview", |
|||
"ok": true, |
|||
"note": "移交预览就绪:线索=1,商机=11308,项目=0,客户=17 (快照已存,步骤4 返还后与此对照)" |
|||
}, |
|||
{ |
|||
"step": "自移交拦截 61019", |
|||
"ok": true, |
|||
"note": "61019 自移交拦截生效:移交方与接收人不能相同" |
|||
}, |
|||
{ |
|||
"step": "移交执行 admin→BUDDY(清零断言)", |
|||
"ok": true, |
|||
"note": "移交执行成功:admin 名下清零;BUDDY 收到 线索=1,商机=11308,项目=0,客户=17 snapshot transferTime=2026-09-09 22:15" |
|||
}, |
|||
{ |
|||
"step": "移交返还(BUDDY 净零 + admin 保底)", |
|||
"ok": true, |
|||
"note": "返还成功:BUDDY 名下回落至移交前(线索=0,商机=0,项目=0,客户=0);admin 各域 ≥ 原快照" |
|||
}, |
|||
{ |
|||
"step": "stats.lastTransferTime 非空", |
|||
"ok": true, |
|||
"note": "出参时间戳到位 lastTransferTime=2026-09-09 22:15:13,lastTransferUser=曾偲青 lastSyncTime=2026-09-08 " |
|||
}, |
|||
{ |
|||
"step": "演示角色就绪", |
|||
"ok": true, |
|||
"note": "演示角色就绪:e2d-演示角色-221451(id=753015620693393408,status=enabled)" |
|||
}, |
|||
{ |
|||
"step": "roles/stats 四计数", |
|||
"ok": true, |
|||
"note": "角色统计:总数=2,启用=2,停用=0,零使用=1" |
|||
}, |
|||
{ |
|||
"step": "assign-roles-batch 增量分配", |
|||
"ok": true, |
|||
"note": "批量增量分配生效:BUDDY roles 含演示角色(id=753015620693393408)" |
|||
}, |
|||
{ |
|||
"step": "roles/copy 停用草稿 _copy_", |
|||
"ok": true, |
|||
"note": "复制产出停用草稿:roleName=e2d-演示角色-221451-副本,roleCode=e2d_role_221451_copy_ApUCLl,status=disabled" |
|||
}, |
|||
{ |
|||
"step": "roles/enable 校验 61021", |
|||
"ok": true, |
|||
"note": "61021 启用校验生效:角色未授权任何菜单,不可启用;请先在角色管理中分配权限资源" |
|||
}, |
|||
{ |
|||
"step": "内置角色禁停保护", |
|||
"ok": true, |
|||
"note": "内置角色保护生效:内置角色不允许停用 (code=61012)" |
|||
}, |
|||
{ |
|||
"step": "BUDDY 角色还原 + 删演示角色", |
|||
"ok": true, |
|||
"note": "BUDDY 角色已还原(基准=[无]);删除角色 2/2 个" |
|||
}, |
|||
{ |
|||
"step": "演示权限点 + 禁删链路就绪", |
|||
"ok": true, |
|||
"note": "演示权限点就绪:id=753015629547569152(父 menu=747858510854750208);禁删链路就绪:演示角色 RD=753015630210269184" |
|||
}, |
|||
{ |
|||
"step": "resources/references 非空", |
|||
"ok": true, |
|||
"note": "引用清单非空:管理员(在职7)" |
|||
}, |
|||
{ |
|||
"step": "resources 停用→启用 roundtrip", |
|||
"ok": true, |
|||
"note": "演示点 停用→启用 roundtrip 成功(幂等)" |
|||
}, |
|||
{ |
|||
"step": "resources 禁删两段 61009+61020", |
|||
"ok": true, |
|||
"note": "禁删两段生效:启用中删除=61009(仅停用状态的资源可删除,请先停用该节点);停用后删除=61020(资源被 1 个角色授权引用,禁止删除;请先在角色管理中解除引用,角色ID:[7530156302" |
|||
}, |
|||
{ |
|||
"step": "演示权限点清理", |
|||
"ok": true, |
|||
"note": "演示角色 RD 已删(级联清引用);演示权限点已停用并删除" |
|||
}, |
|||
{ |
|||
"step": "推送 R1 草稿(仅步骤①,嵌套绑定)", |
|||
"ok": true, |
|||
"note": "R1 草稿就绪:id=753015640054300672,编码=PUSH_RULE_03,版本=V1.0" |
|||
}, |
|||
{ |
|||
"step": "D-01 无步骤④发布(回归)", |
|||
"ok": true, |
|||
"note": "D-01 修复验证:无步骤④发布成功(status=2 发布中)" |
|||
}, |
|||
{ |
|||
"step": "推送 R2 草稿 + 详情回显", |
|||
"ok": true, |
|||
"note": "R2 草稿 4 步骤回显一致:basic=方案评审,校验行=1,映射行=1,createMode=auto,applyScope=2 条件值=投标阶段" |
|||
}, |
|||
{ |
|||
"step": "推送 R2 发布 + versions", |
|||
"ok": true, |
|||
"note": "R2 发布成功:versions 1 行,当前 V1.0 status=2 发布中" |
|||
}, |
|||
{ |
|||
"step": "推送 64029 同名发布中唯一", |
|||
"ok": true, |
|||
"note": "64029 同名称发布中唯一生效:已存在同名称的发布中规则:e2d-推送R2-221451" |
|||
}, |
|||
{ |
|||
"step": "推送 64030 创建方式值域", |
|||
"ok": true, |
|||
"note": "64030 创建方式值域校验生效:项目创建方式非法(可选:自动创建/用户交互/关联已有项目)" |
|||
}, |
|||
{ |
|||
"step": "推送 copy 新编码 V1.0 草稿", |
|||
"ok": true, |
|||
"note": "复制成功:新编码=PUSH_RULE_07,V1.0 草稿(id=753015647998312448)" |
|||
}, |
|||
{ |
|||
"step": "推送 disable + 64027 仅草稿可删", |
|||
"ok": true, |
|||
"note": "停用成功 + 仅草稿可删拦截生效(64027 仅草稿版本可删除)" |
|||
}, |
|||
{ |
|||
"step": "推送草稿清理", |
|||
"ok": true, |
|||
"note": "清理草稿 3/3(R1/R2 已发布转停用留存,e2d- 前缀可辨)" |
|||
}, |
|||
{ |
|||
"step": "查重 D1 发布 + referenceCount 出参", |
|||
"ok": true, |
|||
"note": "D1 发布成功:status=2,referenceCount=0(出参字段在位)" |
|||
}, |
|||
{ |
|||
"step": "查重 64031 属地缺对象", |
|||
"ok": true, |
|||
"note": "64031 属地档校验生效:按项目属地范围须填写适用对象" |
|||
}, |
|||
{ |
|||
"step": "查重 64031 仅全部商机可设默认", |
|||
"ok": true, |
|||
"note": "64031 仅全部商机可设默认生效:仅全部商机范围的规则可设为默认" |
|||
}, |
|||
{ |
|||
"step": "引用计数草稿引用入账", |
|||
"ok": true, |
|||
"note": "引用计数入账:detail.referenceCount=1,versions V1.0 referenceCount=1(草稿引用也计入)" |
|||
}, |
|||
{ |
|||
"step": "草稿不可被正式引用 64030", |
|||
"ok": true, |
|||
"note": "64030 草稿查重规则引用拦截生效:引用的项目查重规则版本非发布中,草稿不可被正式引用" |
|||
}, |
|||
{ |
|||
"step": "查重状态机 copy/删/停用/64033", |
|||
"ok": true, |
|||
"note": "复制(新编码 V1.0 草稿)→ 草稿可删 ✓;停用 D1 → 删除被拦(64033 仅草稿版本可删除)" |
|||
}, |
|||
{ |
|||
"step": "查重草稿清理", |
|||
"ok": true, |
|||
"note": "清理推送/查重草稿 3/3,D1.referenceCount 回落=0" |
|||
}, |
|||
{ |
|||
"step": "项目规则 current 5 区块", |
|||
"ok": true, |
|||
"note": "项目规则聚合回显:stageAliases=2行,poolMembers=0行,conflictPull=对象,stageReminds=0行,bidConfigs=0行" |
|||
}, |
|||
{ |
|||
"step": "项目规则 分区保存 + 回显", |
|||
"ok": true, |
|||
"note": "阶段别名保存并回显:2 行,S1.alias=e2d别名一-221451" |
|||
}, |
|||
{ |
|||
"step": "项目规则 64036 比例校验", |
|||
"ok": true, |
|||
"note": "64036 比例校验生效:标底方认定比例须在 0-100 之间" |
|||
}, |
|||
{ |
|||
"step": "线索池建池(三类异人成员提交)", |
|||
"ok": true, |
|||
"note": "演示池就绪:id=753015674674085888,名称=e2d-演示池-221451,部门=744636475731083264(三类成员已提交)" |
|||
}, |
|||
{ |
|||
"step": "线索池三类成员回填", |
|||
"ok": true, |
|||
"note": "三类成员回填一致:负责人=罗伟健,协作人=1 人,部门成员=1 人(uid=744842317625556992)" |
|||
}, |
|||
{ |
|||
"step": "线索池导出 xlsx(ZIP 魔数)", |
|||
"ok": true, |
|||
"note": "导出成功:4202 字节,ZIP 魔数 PK ✓(xlsx)" |
|||
}, |
|||
{ |
|||
"step": "线索池导入 BatchResult", |
|||
"ok": true, |
|||
"note": "导入成功:BatchResult total=1,success=1,fail=0" |
|||
}, |
|||
{ |
|||
"step": "线索池列表可见(演示+导入)", |
|||
"ok": true, |
|||
"note": "列表可见:演示池 + 导入池均在(共 e2d- 池 3 个)" |
|||
}, |
|||
{ |
|||
"step": "字典种子 opp_stage 06/07 + opportunity_status", |
|||
"ok": true, |
|||
"note": "字典种子在位:opp_stage 7 值含 06技术交流/07竞品打压;opportunity_status 4 值(推进中、暂缓中、已关闭、已转项目)" |
|||
}, |
|||
{ |
|||
"step": "线索池清理", |
|||
"ok": true, |
|||
"note": "清理演示池/导入池 2/2" |
|||
}, |
|||
{ |
|||
"step": "网络层零旧契约形态", |
|||
"ok": true, |
|||
"note": "bad_method=[] old_style=[] (共145请求)" |
|||
}, |
|||
{ |
|||
"step": "v3 saveOrUpdate 载荷零 value/isDefault", |
|||
"ok": true, |
|||
"note": "save提交=3次 bad=[]" |
|||
} |
|||
], |
|||
"all_pass": true |
|||
} |
|||
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 37 KiB |
@ -0,0 +1,245 @@ |
|||
# A7 后台管理验收步骤清单(a7-acceptance · 15 页全分区) |
|||
|
|||
- **被测对象**:`demo/index.html`(15 tab 统一版)× 测试环境 `https://ai.itc.vip/crm-api` |
|||
- **使用方法**:双击打开 demo → 页头「uid 登录」(默认管理员罗伟健)→ 按下表逐 tab 从上到下点每个步骤按钮 → |
|||
输出框**绿色(✅/提示)= PASS**,**红色(失败 + code=)= 候选缺陷** → 结果列填 PASS / FAIL,FAIL 的按《验收结果汇总.md》格式记缺陷。 |
|||
- **纪律**:数字类断言(条数/计数)随库内数据浮动,以**相对变化**为准;每 tab 最后的「清理」步骤务必执行,不留演示数据。 |
|||
|
|||
|
|||
--- |
|||
|
|||
## 用户管理(user)· A7-1 用户管理 |
|||
|
|||
> 覆盖:用户列表 / 统计出参 / 移交全链路 / 时间戳 |
|||
|
|||
| 步骤 | 操作 | 说明 / 预期 | 结果(PASS/FAIL) | 备注 | |
|||
|---|---|---|---|---| |
|||
| 步骤1 | 移交预览(admin 名下各域计数) | — | | | |
|||
| 步骤2 | 自移交拦截(userId=receiverId → 期望 61019) | — | | | |
|||
| 步骤3 | 移交执行 admin→BUDDY(全域计数清零) | — | | | |
|||
| 步骤4 | 移交返还 BUDDY→admin(计数还原) | — | | | |
|||
| 步骤5 | 用户统计出参(lastTransferTime 非空) | — | | | |
|||
|
|||
--- |
|||
|
|||
## 同步任务(sync)· A7-1-2 同步任务(二期票01) |
|||
|
|||
> 覆盖:任务化契约 / 分页倒序 / 详情五块 / 设置读写与边界 / lastSyncTime 切表 |
|||
|
|||
| 步骤 | 操作 | 说明 / 预期 | 结果(PASS/FAIL) | 备注 | |
|||
|---|---|---|---|---| |
|||
| 步骤1 | 发起同步任务(任务编号格式 + 异步终态轮询) | — | | | |
|||
| 步骤2 | 同步记录分页(筛选 + 开始时间倒序断言) | — | | | |
|||
| 步骤3 | 任务详情(执行结果/失败对象/接口错误/处理结果/进度五块) | — | | | |
|||
| 步骤4 | 读取同步设置(默认值 + 派生出参) | — | | | |
|||
| 步骤5 | 保存合法设置(interval=2 次数=3 并发=1,回显断言) | — | | | |
|||
| 步骤6 | 边界校验(间隔<1min / 次数<1 → 期望 61022) | — | | | |
|||
| 步骤7 | 还原设置(不污染环境) | — | | | |
|||
| 步骤8 | lastSyncTime 数据源切换(任务表最新成功行口径)+ 下次执行时间推导 | — | | | |
|||
|
|||
--- |
|||
|
|||
## 角色管理(role)· A7-2 角色管理 |
|||
|
|||
> 覆盖:统计四计数 / 批量增量分配 / copy / 启停校验 / 内置保护 |
|||
|
|||
| 步骤 | 操作 | 说明 / 预期 | 结果(PASS/FAIL) | 备注 | |
|||
|---|---|---|---|---| |
|||
| 步骤0 | 准备演示角色(幂等建 e2d-演示角色) | — | | | |
|||
| 步骤1 | 角色统计(四计数) | — | | | |
|||
| 步骤2 | 批量增量分配给 BUDDY(assign-roles-batch) | — | | | |
|||
| 步骤3 | 复制演示角色(期望停用草稿 roleCode 带 _copy_) | — | | | |
|||
| 步骤4 | 启用无授权副本(期望 61021 两道校验) | — | | | |
|||
| 步骤5 | 停用内置管理员角色(期望内置保护拦截) | — | | | |
|||
| 步骤6 | 恢复 BUDDY 角色 + 删除演示角色(在职守卫通路) | — | | | |
|||
|
|||
--- |
|||
|
|||
## 权限点管理(res)· A7-2 权限点管理 |
|||
|
|||
> 覆盖:引用清单 / 停启 roundtrip / 禁删两段 |
|||
|
|||
| 步骤 | 操作 | 说明 / 预期 | 结果(PASS/FAIL) | 备注 | |
|||
|---|---|---|---|---| |
|||
| 步骤0 | 准备演示权限点 + 禁删链路(演示角色 RD) | — | | | |
|||
| 步骤1 | 商机管理目录引用清单(期望 ≥1 角色) | — | | | |
|||
| 步骤2 | 演示点停用→启用 roundtrip | — | | | |
|||
| 步骤3 | 删除两段拦截(启用删 61009 / 停用后删 61020) | — | | | |
|||
| 步骤4 | 清理(删 RD 级联清引用 → 停用并删演示点) | — | | | |
|||
|
|||
--- |
|||
|
|||
## 方案推送规则(push)· A7-3 方案推送规则 |
|||
|
|||
> 覆盖:状态机全链(草稿/发布/64029/64030/copy/64027) |
|||
|
|||
| 步骤 | 操作 | 说明 / 预期 | 结果(PASS/FAIL) | 备注 | |
|||
|---|---|---|---|---| |
|||
| 步骤1 | 新建 R1 草稿(仅步骤①,无步骤④) | — | | | |
|||
| 步骤2 | 发布 R1(D-01 回归:期望成功非 500) | — | | | |
|||
| 步骤3 | 新建 R2 完整 4 步骤草稿 + 详情回显 | — | | | |
|||
| 步骤4 | 发布 R2 + versions 版本列表 | — | | | |
|||
| 步骤5 | 同名发布中唯一(期望 64029) | — | | | |
|||
| 步骤6 | 创建方式非法(期望 64030) | — | | | |
|||
| 步骤7 | 复制 R2(新编码 V1.0 草稿) | — | | | |
|||
| 步骤8 | 停用 R2 + 删除已停用(期望 64027 仅草稿可删) | — | | | |
|||
| 步骤9 | 清理草稿(R3/R4/copy) | — | | | |
|||
|
|||
--- |
|||
|
|||
## 项目查重规则(dedup)· A7-3 项目查重规则 |
|||
|
|||
> 覆盖:发布与引用计数 / 64031 两路 / 64030 / 64033 |
|||
|
|||
| 步骤 | 操作 | 说明 / 预期 | 结果(PASS/FAIL) | 备注 | |
|||
|---|---|---|---|---| |
|||
| 步骤1 | 新建 D1(全部商机)→ 发布 | — | | | |
|||
| 步骤2 | 属地档缺适用对象(期望 64031) | — | | | |
|||
| 步骤3 | 属地档设默认(期望 64031 仅全部商机可设默认) | — | | | |
|||
| 步骤4 | 推送规则 R5 草稿引用 D1 → referenceCount=1(草稿引用入账) | — | | | |
|||
| 步骤5 | 引用草稿 D2 发布 R6(期望 64030 草稿不可被正式引用) | — | | | |
|||
| 步骤6 | 复制→删草稿 / 停用 D1 / 删已停用(期望 64033) | — | | | |
|||
| 步骤7 | 清理(删 R5/R6/D2 草稿,refCount 回落) | — | | | |
|||
|
|||
--- |
|||
|
|||
## 项目规则(projrule)· A7-3 项目规则 |
|||
|
|||
> 覆盖:current 5 区块聚合 / 分区保存 / 64036 比例校验 |
|||
|
|||
| 步骤 | 操作 | 说明 / 预期 | 结果(PASS/FAIL) | 备注 | |
|||
|---|---|---|---|---| |
|||
| 步骤1 | 读当前配置(5 区块) | — | | | |
|||
| 步骤2 | 保存阶段别名(2 行)→ 回显断言 | — | | | |
|||
| 步骤3 | 标底比例越界 150(期望 64036) | — | | | |
|||
|
|||
--- |
|||
|
|||
## 线索池(pool)· A7-3 线索规则·线索池 |
|||
|
|||
> 覆盖:三类异人成员 / 导出导入 / 列表 / 清理 |
|||
|
|||
| 步骤 | 操作 | 说明 / 预期 | 结果(PASS/FAIL) | 备注 | |
|||
|---|---|---|---|---| |
|||
| 步骤1 | 建演示池(负责人/协作人/部门成员三类异人) | — | | | |
|||
| 步骤2 | 详情回填断言(三类成员名单) | — | | | |
|||
| 步骤3 | 导出(xlsx 字节数 + ZIP 魔数) | — | | | |
|||
| 步骤4 | 导入上方文件(逐行 BatchResult) | 脚本预先准备 12 列模板 xlsx(1 条 e2d-导入池,部门取空闲第二顺位避让 64003) | | | |
|||
| 步骤5 | 列表断言(演示池/导入池可见) | — | | | |
|||
| 步骤6 | 清理(删演示池/导入池) | — | | | |
|||
|
|||
--- |
|||
|
|||
## 公海绑定数(poolrule)· A7-3-4 公海规则「绑定商机(数量)」(二期票03) |
|||
|
|||
> 覆盖:部门专用规则发布 / boundOppCount 增量 / 默认兜底 / 清理 |
|||
|
|||
| 步骤 | 操作 | 说明 / 预期 | 结果(PASS/FAIL) | 备注 | |
|||
|---|---|---|---|---| |
|||
| 步骤1 | 建演示部门专用规则并发布(e2d-绑定数规则;部门被占则回退既有发布中专用规则) | — | | | |
|||
| 步骤2 | 建演示商机 A → detail.boundOppCount = base+1 | POST /api/opportunity 六必填+省市 code(照 opportunity-e2e 先例) | | | |
|||
| 步骤3 | 建演示商机 B → detail.boundOppCount = base+2 | — | | | |
|||
| 步骤4 | 默认规则兜底口径(发布中默认规则计数不因本部门建商机而增) | 无发布中默认规则时降级为提示(单测已覆盖口径) | | | |
|||
| 步骤5 | 清理(关停演示商机 close / 停用演示规则) | 商机无删除端点(API 正门终态=关闭);规则发布中→停用留存可辨 | | | |
|||
|
|||
--- |
|||
|
|||
## 全局日志(glog)· A7-4 日志管理(二期票02 · crm-log) |
|||
|
|||
> 覆盖:AOP 采集 / 操作·登录·批量日志 / 结果文件 / 权限口径 |
|||
|
|||
| 步骤 | 操作 | 说明 / 预期 | 结果(PASS/FAIL) | 备注 | |
|||
|---|---|---|---|---| |
|||
| 步骤1 | 建/删演示角色(AOP 写端点采集素材) | — | | | |
|||
| 步骤2 | 操作日志分页(operatorName/opType 筛选命中 + 逐字段) | — | | | |
|||
| 步骤3 | 退出登录(内联埋点)→ 登录日志「退出」行断言 | debug/token 不走登录采集点(票内已核实),退出钩子按请求登录态采集——验证后自动重铸 token | | | |
|||
| 步骤4 | 线索池导出 → batch/page「导出」行(result+结果文件 id) | — | | | |
|||
| 步骤5 | 导入上方文件 → batch/page「导入」行(BatchResult 计数) | 脚本预置 12 列模板 xlsx(1 条 e2d-导入池,部门取空闲顺位避让 64003) | | | |
|||
| 步骤6 | 结果文件下载 /api/log/batch/file?id=(ZIP 魔数) | — | | | |
|||
| 步骤7 | 打包下载 /api/log/batch/download ids[](ZIP 魔数) | — | | | |
|||
| 步骤8 | 不存在文件 id 错误形态(40401/404 形态) | — | | | |
|||
| 步骤9 | 非管理员(BUDDY)调三 page(期望权限拒绝) | — | | | |
|||
|
|||
--- |
|||
|
|||
## 字典种子验证(seed)· A7-5 字典种子验证 |
|||
|
|||
> 覆盖:opp_stage 06/07 + opportunity_status 4 值 |
|||
|
|||
| 步骤 | 操作 | 说明 / 预期 | 结果(PASS/FAIL) | 备注 | |
|||
|---|---|---|---|---| |
|||
| 步骤1 | 校验种子可见性 | — | | | |
|||
|
|||
--- |
|||
|
|||
## 分组管理(dgroup)· A7-5 数据字典·分组管理(v3 契约) |
|||
|
|||
> 覆盖:新建/编辑/启停/删除/设默认值(仅二级项) |
|||
|
|||
本页为主界面操作(无联测步骤面板),对照原型逐控件核对: |
|||
|
|||
可操作入口:查询、+ 新建分组、上一页、下一页 |
|||
|
|||
| 操作 | 预期 | 结果(PASS/FAIL) | 备注 | |
|||
|---|---|---|---| |
|||
| 逐控件操作并观察列表/弹窗反馈 | 与原型 A7 对应页一致(弹窗字段、错误码文案、列表列头) | | | |
|||
|
|||
--- |
|||
|
|||
## 字典项管理(ditem)· A7-5 数据字典·字典项管理(v3 契约) |
|||
|
|||
> 覆盖:无键值自动编码 / 组内名称唯一 63007 / 复合子项 / 63013 禁停 |
|||
|
|||
本页为主界面操作(无联测步骤面板),对照原型逐控件核对: |
|||
|
|||
可操作入口:查询、+ 新建字典项、上一页、下一页 |
|||
|
|||
| 操作 | 预期 | 结果(PASS/FAIL) | 备注 | |
|||
|---|---|---|---| |
|||
| 逐控件操作并观察列表/弹窗反馈 | 与原型 A7 对应页一致(弹窗字段、错误码文案、列表列头) | | | |
|||
|
|||
--- |
|||
|
|||
## 两级树(dtree)· A7-5 数据字典·两级树 |
|||
|
|||
> 覆盖:一级列表 + 行内二级展开 |
|||
|
|||
本页为主界面操作(无联测步骤面板),对照原型逐控件核对: |
|||
|
|||
可操作入口:刷新 |
|||
|
|||
| 操作 | 预期 | 结果(PASS/FAIL) | 备注 | |
|||
|---|---|---|---| |
|||
| 逐控件操作并观察列表/弹窗反馈 | 与原型 A7 对应页一致(弹窗字段、错误码文案、列表列头) | | | |
|||
|
|||
--- |
|||
|
|||
## 引用计数联测(dref)· A7-5 数据字典·引用计数联测 |
|||
|
|||
> 覆盖:客户引用 +1 / 换引用迁移 / 引用项禁删 63009 / 归档保留计数 |
|||
|
|||
| 步骤 | 操作 | 说明 / 预期 | 结果(PASS/FAIL) | 备注 | |
|||
|---|---|---|---|---| |
|||
| 步骤0 | 准备演示字典项(幂等建 e2d_type_a / e2d_type_b) | 走 POST /api/dict/item/saveOrUpdate:名称组内唯一(63006/63007 幂等可重跑) | | | |
|||
| 步骤1 | 新建客户引用此字典 | POST /api/customer/create(confirmSimilar=true)→ 字典项 referenced=true(ref_count +1,同事务入账) | | | |
|||
| 步骤2 | 编辑客户换引用到 e2d_type_b | POST /api/customer/edit?id=(CAS version)→ 旧项 decrement、新项 increment(applyFieldChange 随迁) | | | |
|||
| 步骤3 | 尝试删除当前被引用项 | POST /api/dict/item/delete → 期望 63009「该字典已被引用,无法删除」 | | | |
|||
| 步骤4 | 归档演示客户(清理) | POST /api/customer/archive?id=;归档保留字典编码引用计数(数据一致性优先) | | | |
|||
|
|||
--- |
|||
|
|||
## 争议留白(待产品/负责人确认,本轮不作为缺陷) |
|||
|
|||
- [ ] 数据共享 3 页(A7-2-3 backup 草稿箱):等原型正式化,未纳入盘点与验收。 |
|||
- [ ] 职务下拉值域来源(OA title 去重 vs 字典分组):挂账待拍板。 |
|||
- [ ] 角色管理「处理使用人员」弹窗「当前参与商机数」列:计数口径未拍板。 |
|||
- [ ] OpportunityPoolRule 列表「所属战队」等冗余列:随 UI 确认后补。 |
|||
- [ ] 幻影挂账 3 端点(POST /depts/save、/depts/delete、/users/admin-role):原型无入口,保留运维用途。 |
|||
- [ ] 项目规则「阶段提醒」区块:原型自注「待补充」。 |
|||
- [ ] 全局日志采集覆盖面 = 写侧(POST)先行,不采「查询」GET;登录日志仅采「退出」钩子(debug/token 不走登录采集点)。 |
|||
|
|||
## 已知形态说明(不算缺陷) |
|||
|
|||
- 同步任务两态容忍:测试环境钉钉凭据不可达时,任务以 all_failed 终态落行(或长时间 running)均为正常形态。 |
|||
- lastSyncTime 口径 = 同步任务表最新成功行 endTime;无成功行时为空。 |
|||
- 操作/登录/批量日志行与同步任务行 = 审计数据,留存不删;其余演示数据一律 e2d- 前缀 + 步骤面板末步清理。 |
|||
@ -0,0 +1,53 @@ |
|||
# A7 验收结果汇总(回喂 AI 修复用) |
|||
|
|||
> **填写说明**:每个 FAIL 步骤登记一条「缺陷块」(§2 格式);字段尽量从 demo 界面**复制原文**(每个 tab 底部「执行日志」面板有每次请求的 URL 与参数,红框里有错误码)。本文件填完后**整体交给 AI** 即可逐条定位修复,不需要再口头补充。 |
|||
|
|||
## 0. 环境与范围 |
|||
|
|||
- 验收人:____ |
|||
- 验收时间:____(起止) |
|||
- demo:`.scratch/a7-acceptance/demo/index.html`(版本/生成时间:2026-09-09) |
|||
- 后端环境:`https://ai.itc.vip/crm-api` |
|||
- 后端仓(AI 定位用,只读):`E:\code\crm-backend-matt` |
|||
- 登录账号:管理员 罗伟健 `739564171091247104`;非管理员 曾偲青(BUDDY) `744842318024015872` |
|||
|
|||
## 1. 总览(逐 tab 填一行) |
|||
|
|||
| tab | 页面 | 步骤总数 | PASS | FAIL | 未验/留白 | |
|||
|---|---|---|---|---|---| |
|||
| user | A7-1 用户管理 | 5 | | | | |
|||
| sync | A7-1-2 同步任务 | 8 | | | | |
|||
| role | A7-2 角色管理 | | | | | |
|||
| res | A7-2 权限点管理 | | | | | |
|||
| push | A7-3 方案推送规则 | | | | | |
|||
| dedup | A7-3 项目查重规则 | | | | | |
|||
| projrule | A7-3 项目规则 | | | | | |
|||
| pool | A7-3 线索池 | | | | | |
|||
| poolrule | A7-3-4 公海绑定数 | 5 | | | | |
|||
| glog | A7-4 全局日志 | 9 | | | | |
|||
| seed | A7-5 字典种子 | | | | | |
|||
| dgroup | A7-5 字典·分组管理 | | | | | |
|||
| ditem | A7-5 字典·字典项管理 | | | | | |
|||
| dtree | A7-5 字典·两级树 | | | | | |
|||
| dref | A7-5 字典·引用计数联测 | 4 | | | | |
|||
|
|||
## 2. 缺陷明细(每个 FAIL 一块,AI 按块定位;复制本块模板续写) |
|||
|
|||
### BUG-01 <一句话现象> |
|||
|
|||
- **所属 tab / 原型分区**:__(如 glog / A7-4 日志管理) |
|||
- **触发步骤**:清单中 tab + 步骤号 + 按钮名(如:glog 步骤6「结果文件下载」) |
|||
- **接口**:`METHOD /api/...`(从 demo 执行日志面板复制) |
|||
- **请求参数**:__(表单串或 JSON 原文) |
|||
- **预期**:__(依据 = 验收步骤清单该步说明 / 原型页) |
|||
- **实际**:__(errbox 红框原文,含 `code=`) |
|||
- **复现方式**:稳定复现 / 概率(__%)/ 依赖前置数据(__) |
|||
- **严重级**:P0 主流程阻断 / P1 功能错误 / P2 展示文案 / P3 建议 |
|||
- **证据**:截图文件名__;控制台报错原文__ |
|||
- **初步归属(可留空由 AI 判)**:demo 前端 / 后端接口 / 测试数据 |
|||
|
|||
### BUG-02 <复制上面整块续写> |
|||
|
|||
## 3. 争议留白(不作为缺陷,等负责人确认) |
|||
|
|||
- 数据共享 3 页(A7-2-3)/职务下拉值域/「参与商机数」列/池规则冗余列/幻影挂账 3 端点/项目规则「阶段提醒」区块——本轮发现的相关问题一律写到这里,不进 §2。 |
|||
@ -1,84 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
"""票05 首轮 FAIL 根因探针(一次性,验证后删):operation/login/batch 日志真实行 + batch/file·download 正确姿势 + publish 被拒原因""" |
|||
import json, urllib.request, urllib.parse, urllib.error, sys, io |
|||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') |
|||
API = "http://localhost:8080" |
|||
ADMIN = "739564171091247104" |
|||
|
|||
T = json.loads(urllib.request.urlopen(f"{API}/api/auth/debug/token?userId={ADMIN}", timeout=15).read().decode())["data"] |
|||
|
|||
def post(path, data=None): |
|||
req = urllib.request.Request(API + path, urllib.parse.urlencode(data or {}).encode(), method="POST") |
|||
req.add_header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8") |
|||
req.add_header("Authorization", "Bearer " + T) |
|||
try: |
|||
with urllib.request.urlopen(req, timeout=30) as r: |
|||
return json.loads(r.read().decode()) |
|||
except urllib.error.HTTPError as e: |
|||
return {"code": -1, "message": f"HTTP {e.code}"} |
|||
|
|||
def raw(path, method="GET", data=None, form_pairs=None): |
|||
body = None |
|||
if form_pairs: |
|||
body = urllib.parse.urlencode(form_pairs).encode() |
|||
elif data: |
|||
body = urllib.parse.urlencode(data).encode() |
|||
req = urllib.request.Request(API + path, body, method=method) |
|||
if body: req.add_header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8") |
|||
req.add_header("Authorization", "Bearer " + T) |
|||
try: |
|||
with urllib.request.urlopen(req, timeout=30) as r: |
|||
b = r.read() |
|||
return {"status": r.status, "len": len(b), "head": b[:120], "magic": b[:2]} |
|||
except urllib.error.HTTPError as e: |
|||
return {"status": e.code, "len": -1, "head": e.read()[:120]} |
|||
|
|||
print("== 1. operation/page 最近 30 行(无筛选,看 opType/operatorName/content 真实值)==") |
|||
d = post("/api/log/operation/page", {"current": 1, "size": 30}) |
|||
for r in (d.get("data") or {}).get("content", [])[:30]: |
|||
print(json.dumps(r, ensure_ascii=False)[:230]) |
|||
|
|||
print("== 2. login/page 最近 15 行(找「退出」行)==") |
|||
d = post("/api/log/login/page", {"current": 1, "size": 15}) |
|||
for r in (d.get("data") or {}).get("content", [])[:15]: |
|||
print(json.dumps(r, ensure_ascii=False)[:230]) |
|||
|
|||
print("== 3. batch/page 最近 5 行(rowId vs fileId)==") |
|||
d = post("/api/log/batch/page", {"current": 1, "size": 5}) |
|||
rows = (d.get("data") or {}).get("content", []) |
|||
for r in rows: |
|||
print(json.dumps(r, ensure_ascii=False)[:230]) |
|||
|
|||
if rows: |
|||
rid = rows[0]["id"] |
|||
print(f"== 4. GET batch/file?id={rid}(传日志行主键)==") |
|||
print(raw(f"/api/log/batch/file?id={rid}")) |
|||
print(f"== 5. POST batch/download ids=[{rid}](单 id)==") |
|||
print(raw("/api/log/batch/download", method="POST", form_pairs=[("ids", str(rid))])) |
|||
if len(rows) >= 2: |
|||
print("== 5b. POST batch/download ids=[两行] ==") |
|||
print(raw("/api/log/batch/download", method="POST", |
|||
form_pairs=[("ids", str(rows[0]["id"])), ("ids", str(rows[1]["id"]))])) |
|||
|
|||
print("== 6. publish 试探(e2d-绑定数规则-probe,部门专用 admin 部门)==") |
|||
d = post("/api/rule/opp-pool-rule/publish", { |
|||
"ruleName": "e2d-绑定数规则-probe", "applyScope": 2, "deptIds": "744841292348915712", |
|||
"isDefault": 0, "allowManualPool": 1, "autoRecycleEnabled": 0, "allowFreeClaim": 1, |
|||
"ruleDesc": "probe 临时"}) |
|||
print(json.dumps(d, ensure_ascii=False)[:400]) |
|||
|
|||
print("== 7. opp-pool-rule/page 最近 20 行(看占用与状态分布)==") |
|||
d = post("/api/rule/opp-pool-rule/page", {"current": 1, "size": 20}) |
|||
for r in (d.get("data") or {}).get("content", [])[:20]: |
|||
print(json.dumps(r, ensure_ascii=False)[:200]) |
|||
|
|||
print("== 8. saveOrUpdate 角色后立即查「修改」行(验证 resolveOpType 顺序 bug)==") |
|||
d = post("/api/roles/saveOrUpdate", {"roleName": "e2d-probe角色-x", "roleCode": "e2d_probe_x", "remark": "probe"}) |
|||
print("saveOrUpdate:", json.dumps(d, ensure_ascii=False)[:120]) |
|||
d = post("/api/log/operation/page", {"current": 1, "size": 5}) |
|||
for r in (d.get("data") or {}).get("content", [])[:5]: |
|||
print(json.dumps(r, ensure_ascii=False)[:200]) |
|||
# 清理 probe 角色 |
|||
d = post("/api/roles/page", {"current": 1, "size": 10, "roleName": "e2d-probe角色-x"}) |
|||
for r in (d.get("data") or {}).get("content", []): |
|||
print("cleanup role:", post("/api/roles/delete", {"id": str(r["id"])}).get("code")) |
|||
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 72 KiB |
@ -0,0 +1,180 @@ |
|||
{ |
|||
"steps": [ |
|||
{ |
|||
"step": "list rows == 9", |
|||
"ok": true, |
|||
"extra": "9" |
|||
}, |
|||
{ |
|||
"step": "list phones masked == 9", |
|||
"ok": true, |
|||
"extra": "9" |
|||
}, |
|||
{ |
|||
"step": "export disabled w/o selection", |
|||
"ok": true, |
|||
"extra": "" |
|||
}, |
|||
{ |
|||
"step": "reveal -> tracking log", |
|||
"ok": true, |
|||
"extra": "1" |
|||
}, |
|||
{ |
|||
"step": "revealed phone plain", |
|||
"ok": true, |
|||
"extra": "1" |
|||
}, |
|||
{ |
|||
"step": "graph cards == 9", |
|||
"ok": true, |
|||
"extra": "9" |
|||
}, |
|||
{ |
|||
"step": "auto edges == 4 (3×100→公司 + 邓伦→胡子)", |
|||
"ok": true, |
|||
"extra": "4" |
|||
}, |
|||
{ |
|||
"step": "top badges == 3", |
|||
"ok": true, |
|||
"extra": "3" |
|||
}, |
|||
{ |
|||
"step": "graph card phones == 9", |
|||
"ok": true, |
|||
"extra": "9" |
|||
}, |
|||
{ |
|||
"step": "edit dots > 0", |
|||
"ok": true, |
|||
"extra": "18" |
|||
}, |
|||
{ |
|||
"step": "level chips == 8 (9 cards, c9 unrec)", |
|||
"ok": true, |
|||
"extra": "8" |
|||
}, |
|||
{ |
|||
"step": "pending add rendered", |
|||
"ok": true, |
|||
"extra": "1" |
|||
}, |
|||
{ |
|||
"step": "single-parent constraint rejected", |
|||
"ok": true, |
|||
"extra": "邓伦 已有上级(胡子),一个节点最多一个上级,请先删除原连线" |
|||
}, |
|||
{ |
|||
"step": "same-level rejected", |
|||
"ok": true, |
|||
"extra": "同级(层级相同)之间不建立上下级连线" |
|||
}, |
|||
{ |
|||
"step": "dup toast 已存在连线 (reverse args)", |
|||
"ok": true, |
|||
"extra": "已存在连线:胡子 ↔ 王强(蓝色实线即是),换一对联系人试试" |
|||
}, |
|||
{ |
|||
"step": "reverse same pair rejected", |
|||
"ok": true, |
|||
"extra": "已存在连线:王大牛 ↔ 张华(蓝色实线即是),换一对联系人试试" |
|||
}, |
|||
{ |
|||
"step": "level picker dialog shown", |
|||
"ok": true, |
|||
"extra": "True" |
|||
}, |
|||
{ |
|||
"step": "c9 leveled 50 + moved to 50 row (y=650)", |
|||
"ok": true, |
|||
"extra": "level=50 y=650" |
|||
}, |
|||
{ |
|||
"step": "edges == 7 (4 committed + 3 pending)", |
|||
"ok": true, |
|||
"extra": "7" |
|||
}, |
|||
{ |
|||
"step": "level toast", |
|||
"ok": true, |
|||
"extra": "已为「江小白」设定层级 50,自动边已重建;连线待保存生效" |
|||
}, |
|||
{ |
|||
"step": "same-level pending auto-cleared on level change", |
|||
"ok": true, |
|||
"extra": "2|6|logged" |
|||
}, |
|||
{ |
|||
"step": "reset clears pending + reverts level (back to 4 edges)", |
|||
"ok": true, |
|||
"extra": "pending=0 edges=4 c9.level=None c9.y=900" |
|||
}, |
|||
{ |
|||
"step": "qa modal rows == 2", |
|||
"ok": true, |
|||
"extra": "2" |
|||
}, |
|||
{ |
|||
"step": "qa added -> rows == 10", |
|||
"ok": true, |
|||
"extra": "10" |
|||
}, |
|||
{ |
|||
"step": "qa modal closed on success", |
|||
"ok": true, |
|||
"extra": "False" |
|||
}, |
|||
{ |
|||
"step": "dirty row highlight", |
|||
"ok": true, |
|||
"extra": "1" |
|||
}, |
|||
{ |
|||
"step": "undo restores", |
|||
"ok": true, |
|||
"extra": "" |
|||
}, |
|||
{ |
|||
"step": "save-all blocked + first-err located", |
|||
"ok": true, |
|||
"extra": "1 | 校验失败:邓伦 — 电话须为 11 位手机号(已定位到首个错误行,不提交任何行)" |
|||
}, |
|||
{ |
|||
"step": "save-all success", |
|||
"ok": true, |
|||
"extra": "保存全部成功" |
|||
}, |
|||
{ |
|||
"step": "edges after job change == 3", |
|||
"ok": true, |
|||
"extra": "3" |
|||
}, |
|||
{ |
|||
"step": "auto-edge delete confirm dialog", |
|||
"ok": true, |
|||
"extra": "True" |
|||
}, |
|||
{ |
|||
"step": "vetoed pair recorded", |
|||
"ok": true, |
|||
"extra": "1" |
|||
}, |
|||
{ |
|||
"step": "real-mouse bottom-dot drag -> pending edge", |
|||
"ok": true, |
|||
"extra": "1" |
|||
}, |
|||
{ |
|||
"step": "top-dot reverse drag -> dup rejected with names", |
|||
"ok": true, |
|||
"extra": "已存在连线:王大牛 ↔ 胡子(蓝色实线即是),换一对联系人试试" |
|||
}, |
|||
{ |
|||
"step": "cancel pending edge via ×", |
|||
"ok": true, |
|||
"extra": "已撤销本次新增连线(未保存)" |
|||
} |
|||
], |
|||
"errors": [] |
|||
} |
|||
@ -0,0 +1,235 @@ |
|||
# -*- coding: utf-8 -*- |
|||
"""票 00 原型 demo Playwright 自检:无 JS 报错 + 关键交互冒烟 + 截图。""" |
|||
import io, os, sys, json |
|||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') |
|||
from playwright.sync_api import sync_playwright |
|||
|
|||
BASE = r'e:\code\crm-backend-matt\.scratch\customer-contact-graph\prototype' |
|||
URL = 'file:///' + os.path.join(BASE, 'prototype-contact-graph.html').replace('\\', '/') |
|||
SHOTS = os.path.join(BASE, 'shots') |
|||
os.makedirs(SHOTS, exist_ok=True) |
|||
|
|||
errors, steps = [], [] |
|||
def step(name, ok, extra=''): |
|||
steps.append({'step': name, 'ok': bool(ok), 'extra': str(extra)}) |
|||
print(('PASS' if ok else 'FAIL'), '|', name, '|', extra) |
|||
|
|||
with sync_playwright() as p: |
|||
b = p.chromium.launch() |
|||
pg = b.new_page(viewport={'width': 1600, 'height': 950}) |
|||
pg.on('console', lambda m: errors.append('console.' + m.type + ': ' + m.text) if m.type == 'error' else None) |
|||
pg.on('pageerror', lambda e: errors.append('pageerror: ' + str(e))) |
|||
pg.goto(URL) |
|||
pg.wait_for_timeout(400) |
|||
|
|||
# 1 列表态:9 行全脱敏 |
|||
rows = pg.locator('#contactTbody tr').count() |
|||
step('list rows == 9', rows == 9, rows) |
|||
masked = pg.locator('.phone-masked').count() |
|||
step('list phones masked == 9', masked == 9, masked) |
|||
step('export disabled w/o selection', pg.locator('#btnExport').is_disabled()) |
|||
pg.screenshot(path=os.path.join(SHOTS, '01-list.png'), full_page=True) |
|||
|
|||
# 2 点击明文 + 埋点日志 |
|||
pg.locator('.phone-masked').first.click() |
|||
pg.wait_for_timeout(200) |
|||
logs = pg.locator('#spLogs .log-item').count() |
|||
step('reveal -> tracking log', logs >= 1, logs) |
|||
plain = pg.locator('.phone-plain').count() |
|||
step('revealed phone plain', plain == 1, plain) |
|||
pg.screenshot(path=os.path.join(SHOTS, '02-reveal.png'), full_page=True) |
|||
|
|||
# 3 图谱态:9 卡 + 公司节点 + 4 auto 边(3×100→公司 + 邓伦→胡子)+ 3 顶层徽标 |
|||
pg.click('#pillGraph') |
|||
pg.wait_for_timeout(500) |
|||
cards = pg.locator('#canvasInner .gcard:not(.company)').count() |
|||
step('graph cards == 9', cards == 9, cards) |
|||
edges = pg.locator('#edgeSvg g.edge-g').count() |
|||
step('auto edges == 4 (3×100→公司 + 邓伦→胡子)', edges == 4, edges) |
|||
badge = pg.locator('.top-badge').count() |
|||
step('top badges == 3', badge == 3, badge) |
|||
zone = pg.locator('#unrecZone .gcard').count() if pg.locator('#unrecZone .gcard').count() else 0 |
|||
gphone = pg.locator('.gcard .gphone').count() |
|||
step('graph card phones == 9', gphone == 9, gphone) |
|||
pg.screenshot(path=os.path.join(SHOTS, '03-graph.png'), full_page=True) |
|||
|
|||
# 4 编辑态:圆点出现 + 拖线逻辑(页面函数直调冒烟) |
|||
pg.click('#btnDraw') |
|||
pg.wait_for_timeout(300) |
|||
dots = pg.locator('.gcard .dot').count() |
|||
step('edit dots > 0', dots > 0, dots) |
|||
chips = pg.locator('.gcard .lv-chip').count() |
|||
step('level chips == 8 (9 cards, c9 unrec)', chips == 8, chips) |
|||
pg.evaluate("tryConnect('c1','c4')") # 王强(100)→胡子(60):层级自动定向 → pending |
|||
pg.wait_for_timeout(200) |
|||
pend = pg.locator('#edgeSvg path.edge-pending').count() |
|||
step('pending add rendered', pend >= 1, pend) |
|||
pg.evaluate("tryConnect('c1','c7')") # 邓伦已有上级(胡子)→ 单父约束拒绝 |
|||
pg.wait_for_timeout(200) |
|||
tsp = pg.locator('#toast').text_content() or '' |
|||
step('single-parent constraint rejected', '已有上级' in tsp and '胡子' in tsp, tsp) |
|||
pg.evaluate("tryConnect('c5','c4')") # 胡子 60 = 张华 60 → 同级拒绝 |
|||
pg.wait_for_timeout(200) |
|||
tss = pg.locator('#toast').text_content() or '' |
|||
step('same-level rejected', '同级' in tss, tss) |
|||
pg.evaluate("tryConnect('c4','c1')") # 反向参数 → 仍命中已存在(无向查重) |
|||
pg.wait_for_timeout(200) |
|||
t = pg.locator('#toast').text_content() or '' |
|||
step('dup toast 已存在连线 (reverse args)', '已存在连线' in t, t) |
|||
pg.evaluate("tryConnect('c5','c8')") # 张华(60)→王大牛(30):层级定向 → pending |
|||
pg.wait_for_timeout(200) |
|||
pg.evaluate("tryConnect('c8','c5')") # 反向同对 → 层级定向后必命中已存在(图恒 DAG,无环可造) |
|||
pg.wait_for_timeout(200) |
|||
trr = pg.locator('#toast').text_content() or '' |
|||
step('reverse same pair rejected', '已存在连线' in trr, trr) |
|||
pg.screenshot(path=os.path.join(SHOTS, '05-cycle.png'), full_page=True) |
|||
|
|||
# 4b 未识别层级连线定级(用户拍板 2026-09-09:连线时输入层级) |
|||
pg.evaluate("tryConnect('c4','c9')") # 胡子→江小白(未设层级)→ 弹层定级 |
|||
pg.wait_for_timeout(300) |
|||
dlgLv = pg.locator('#dlgOverlay').evaluate('el => el.classList.contains("show")') |
|||
step('level picker dialog shown', dlgLv is True, dlgLv) |
|||
pg.fill('#lvlInput', '50') # 0-100,50 → 公司下方第二档(60,50,30 降序) |
|||
pg.locator('#dlgFoot button.btn-primary').click() # 确定并连线 |
|||
pg.wait_for_timeout(300) |
|||
lvl = pg.evaluate("byId('c9').level") |
|||
y9 = pg.evaluate("cardPos['c9'].y") |
|||
eff = pg.locator('#edgeSvg g.edge-g').count() |
|||
step('c9 leveled 50 + moved to 50 row (y=650)', lvl == 50 and y9 == 650, 'level=%s y=%s' % (lvl, y9)) |
|||
# committed 4 + pending(c1→c4, c5→c8, c4→c9)= 7;c9 定级 50:相邻最高档 60 三人多候选 → 不自动连;方向由层级定(50<60 → 胡子为上级) |
|||
step('edges == 7 (4 committed + 3 pending)', eff == 7, eff) |
|||
t5 = pg.locator('#toast').text_content() or '' |
|||
step('level toast', '设定层级' in t5, t5) |
|||
# 4c 层级变化 → 失效线自动清理(修订⑤补丁):c9 50→60 与 c4 60 同级,c4→c9 pending 被清 |
|||
pg.evaluate("byId('c9').job='商务经理'; byId('c9').level=60; rebuildAutoEdges(); renderGraph(); renderPanelState();") |
|||
pg.wait_for_timeout(200) |
|||
pend4 = pg.locator('#edgeSvg path.edge-pending').count() |
|||
eff4 = pg.locator('#edgeSvg g.edge-g').count() |
|||
logs4 = pg.locator('#spLogs').text_content() or '' |
|||
step('same-level pending auto-cleared on level change', pend4 == 2 and eff4 == 6 and '不再满足上下级关系' in logs4, '%d|%d|%s' % (pend4, eff4, 'logged' if '不再满足' in logs4 else 'no-log')) |
|||
pg.screenshot(path=os.path.join(SHOTS, '05b-level.png'), full_page=True) |
|||
|
|||
# 5 重置清 pending |
|||
pg.click('#btnGraphReset') |
|||
pg.wait_for_timeout(200) |
|||
pg.locator('#dlgFoot button.btn-primary').click() |
|||
pg.wait_for_timeout(200) |
|||
pend2 = pg.locator('#edgeSvg path.edge-pending').count() |
|||
lvl2 = pg.evaluate("byId('c9').level") |
|||
y9b = pg.evaluate("cardPos['c9'] ? cardPos['c9'].y : -1") |
|||
step('reset clears pending + reverts level (back to 4 edges)', pend2 == 0 and pg.locator('#edgeSvg g.edge-g').count() == 4 and lvl2 is None and y9b == 900, |
|||
'pending=%d edges=%d c9.level=%s c9.y=%s' % (pend2, pg.locator('#edgeSvg g.edge-g').count(), lvl2, y9b)) |
|||
pg.click('#btnDraw') # 完成绘制(无 dirty 直接退) |
|||
pg.wait_for_timeout(200) |
|||
|
|||
# 6 6.2 快速添加:必填校验 + 成功刷新 |
|||
pg.click('#pillList') |
|||
pg.click('#btnAddContact') # 6.2 入口 = 原型 u193「添加联系人」 |
|||
pg.wait_for_timeout(200) |
|||
nrows = pg.locator('#qaTbody tr').count() |
|||
step('qa modal rows == 2', nrows == 2, nrows) |
|||
pg.fill('#qaTbody tr[data-qa="0"] input[data-qf="name"]', '测试员') |
|||
pg.select_option('#qaTbody tr[data-qa="0"] select[data-qf="cat"]', '市场') |
|||
pg.wait_for_timeout(150) |
|||
pg.select_option('#qaTbody tr[data-qa="0"] select[data-qf="job"]', '市场专员') |
|||
pg.click('#qaSubmit') |
|||
pg.wait_for_timeout(300) |
|||
rows2 = pg.locator('#contactTbody tr').count() |
|||
step('qa added -> rows == 10', rows2 == 10, rows2) |
|||
ov = pg.locator('#qaOverlay').evaluate('el => el.classList.contains("show")') |
|||
step('qa modal closed on success', ov is False, ov) |
|||
pg.screenshot(path=os.path.join(SHOTS, '06-qa-added.png'), full_page=True) |
|||
|
|||
# 7 6.3 页内编辑:dirty + 撤销 + 保存全部 |
|||
pg.click('#btnEditList') |
|||
pg.wait_for_timeout(200) |
|||
pg.fill('#contactTbody tr[data-id="c9"] input[data-f="name"]', '江小百') |
|||
pg.wait_for_timeout(150) |
|||
dirty = pg.locator('.dirty-row').count() |
|||
step('dirty row highlight', dirty == 1, dirty) |
|||
pg.click('.undo-link') |
|||
pg.wait_for_timeout(150) |
|||
step('undo restores', (pg.locator('#contactTbody tr[data-id="c9"] input[data-f="name"]').input_value()) == '江小白') |
|||
pg.fill('#contactTbody tr[data-id="c7"] input[data-f="phone"]', '123') # 非法电话 → 首错定位 |
|||
pg.wait_for_timeout(100) |
|||
pg.click('#btnSaveList') |
|||
pg.wait_for_timeout(300) |
|||
errrows = pg.locator('#contactTbody tr.err-row').count() |
|||
t3 = pg.locator('#toast').text_content() or '' |
|||
step('save-all blocked + first-err located', errrows == 1 and '校验失败' in t3, '%d | %s' % (errrows, t3)) |
|||
pg.fill('#contactTbody tr[data-id="c7"] input[data-f="phone"]', '13127283272') |
|||
pg.wait_for_timeout(100) |
|||
pg.click('#btnSaveList') |
|||
pg.wait_for_timeout(400) |
|||
t4 = pg.locator('#toast').text_content() or '' |
|||
step('save-all success', '保存全部成功' in t4, t4) |
|||
pg.click('#btnEditList') |
|||
pg.wait_for_timeout(200) |
|||
|
|||
# 8 职务变更 → 自动边重建(c7 邓伦:采购专员→市场经理) |
|||
pg.click('#btnEditList') |
|||
pg.wait_for_timeout(200) |
|||
pg.select_option('#contactTbody tr[data-id="c7"] select[data-f="jobFull"]', '市场|市场经理') |
|||
pg.wait_for_timeout(150) |
|||
pg.click('#btnSaveList') |
|||
pg.wait_for_timeout(400) |
|||
pg.click('#pillGraph') |
|||
pg.wait_for_timeout(500) |
|||
edges2 = pg.locator('#edgeSvg g.edge-g').count() |
|||
# 单父模型:基数 4;c7 邓伦 采购30→市场60:原 auto 父(胡子)唯一性消失 → c4→c7 重建消失;60 多候选(100 三人)不连 → 4-1 = 3 |
|||
step('edges after job change == 3', edges2 == 3, edges2) |
|||
pg.screenshot(path=os.path.join(SHOTS, '07-jobchange-graph.png'), full_page=True) |
|||
|
|||
# 9 删除系统边二次确认 → vetoed(first = 王强→公司,100→公司系统边) |
|||
pg.click('#btnDraw') |
|||
pg.wait_for_timeout(300) |
|||
pg.locator('#edgeSvg g.edge-g').first.locator('.edge-del').click() |
|||
pg.wait_for_timeout(200) |
|||
dlg = pg.locator('#dlgOverlay').evaluate('el => el.classList.contains("show")') |
|||
step('auto-edge delete confirm dialog', dlg is True, dlg) |
|||
pg.locator('#dlgFoot button.btn-primary').click() |
|||
pg.wait_for_timeout(200) |
|||
pg.click('#btnGraphSave') |
|||
pg.wait_for_timeout(300) |
|||
veto = pg.locator('#spState .edge-chip.veto').count() |
|||
step('vetoed pair recorded', veto >= 1, veto) |
|||
pg.click('#btnDraw') |
|||
pg.wait_for_timeout(200) |
|||
pg.screenshot(path=os.path.join(SHOTS, '08-veto.png'), full_page=True) |
|||
|
|||
# 10 真实鼠标全链路:底点正向拖 + 顶点反向拖 + 重复拒绝(验证 mousedown→move→up 链路与命中区) |
|||
pg.click('#btnDraw') # 重新进编辑 |
|||
pg.wait_for_timeout(250) |
|||
d1 = pg.locator('.gcard[data-id="c4"] .dot-bottom').bounding_box() |
|||
c8 = pg.locator('.gcard[data-id="c8"]').bounding_box() |
|||
pg.mouse.move(d1['x'] + d1['width'] / 2, d1['y'] + d1['height'] / 2) |
|||
pg.mouse.down() |
|||
pg.mouse.move(c8['x'] + c8['width'] / 2, c8['y'] + 12, steps=14) |
|||
pg.mouse.up() |
|||
pg.wait_for_timeout(300) |
|||
pm = pg.locator('#edgeSvg path.edge-pending').count() |
|||
step('real-mouse bottom-dot drag -> pending edge', pm >= 1, pm) |
|||
d2 = pg.locator('.gcard[data-id="c8"] .dot-top').bounding_box() |
|||
c4b = pg.locator('.gcard[data-id="c4"]').bounding_box() |
|||
pg.mouse.move(d2['x'] + d2['width'] / 2, d2['y'] + d2['height'] / 2) |
|||
pg.mouse.down() |
|||
pg.mouse.move(c4b['x'] + c4b['width'] / 2, c4b['y'] + 12, steps=14) |
|||
pg.mouse.up() |
|||
pg.wait_for_timeout(300) |
|||
tr6 = pg.locator('#toast').text_content() or '' |
|||
step('top-dot reverse drag -> dup rejected with names', '已存在连线' in tr6 and '胡子' in tr6, tr6) |
|||
pg.locator('#edgeSvg g.edge-g').last.locator('.edge-del').click() # 撤销 pending |
|||
pg.wait_for_timeout(250) |
|||
tr7 = pg.locator('#toast').text_content() or '' |
|||
step('cancel pending edge via ×', '撤销本次新增' in tr7, tr7) |
|||
pg.click('#btnDraw') # 无 dirty 直接退 |
|||
pg.wait_for_timeout(200) |
|||
pg.screenshot(path=os.path.join(SHOTS, '09-realdrag.png'), full_page=True) |
|||
|
|||
b.close() |
|||
|
|||
print('\n==== console/page errors ====') |
|||
print(json.dumps(errors, ensure_ascii=False, indent=1) if errors else '(none)') |
|||
print('==== summary ====%d steps, %d failed' % (len(steps), sum(1 for s in steps if not s['ok']))) |
|||
with open(os.path.join(BASE, 'selfcheck-result.json'), 'w', encoding='utf-8') as f: |
|||
json.dump({'steps': steps, 'errors': errors}, f, ensure_ascii=False, indent=1) |
|||
|
After Width: | Height: | Size: 332 KiB |
|
After Width: | Height: | Size: 332 KiB |
|
After Width: | Height: | Size: 307 KiB |
|
After Width: | Height: | Size: 330 KiB |
|
After Width: | Height: | Size: 330 KiB |
|
After Width: | Height: | Size: 342 KiB |
|
After Width: | Height: | Size: 315 KiB |
|
After Width: | Height: | Size: 314 KiB |
|
After Width: | Height: | Size: 306 KiB |
@ -0,0 +1,55 @@ |
|||
pre_clean: {'roles': 0, 'rules_deleted': 0, 'rules_disabled': 0, 'pools': 0} |
|||
PASS 登录 debug/token + /auth/me |
|||
PASS 同步任务 launch(任务编号格式 + 61023 两态容忍) | 任务化契约验证:taskId=753014778993049600,taskNo=SYNC-0909-02(SYNC-MMdd-序号 ✓) 异步终态:running(75s 未终态,任务行在位即可) |
|||
PASS 同步记录分页 + 开始时间倒序 | rows=2 desc=True first=2026-09-09 02:00:00 |
|||
PASS 任务详情五块结构(含 failDetails 失败明细) | 详情五块结构在位:taskNo=SYNC-0909-02,status=同步中,进度=5%,失败明细 0 条 |
|||
PASS 同步设置回显(派生 plan/nextExecuteTime/lastModifierName) | 设置回显:autoSyncEnabled=true,dailyTime=02:00,retryIntervalMinutes=10,retryMaxTimes=2,concurrency=1 派生:当前计划=每日 02:00(运行中),下次 |
|||
PASS 设置保存回显(interval=2 次数=3 并发=1) | 合法设置保存并回显:interval=2,次数=3,并发=1(autoSyncEnabled/dailyTime 保留原值不污染调度) |
|||
PASS 设置边界 61022 双路(间隔<1min / 次数<1 拒) | 61022 边界校验两路生效:间隔<1min 拒(重试间隔不能小于 1 分钟);次数<1 拒(重试次数不能小于 1 次) |
|||
PASS 设置还原(不污染调度原值) | 设置已还原(interval=10,次数=2,并发=1,dailyTime=02:00) |
|||
PASS lastSyncTime 切表口径 + 下次执行时间推导 | lastSyncTime 验证:与最新成功任务行 endTime 一致(2026-09-08 17:18:35)——切表生效 下次执行时间推导:autoSyncEnabled=true,nextExecuteTime=2026-09-10 02:00:00(推 |
|||
PASS AOP 写端点采集素材(建/删演示角色) | 写操作完成:建/删演示角色 e2d-日志演示角色-221152(等待采集落库后断言) |
|||
PASS 操作日志分页(新增/删除行命中 + 时间范围逐字段) | AOP 采集行命中:新增行 content=/api/roles/saveOrUpdate,操作人=罗伟健,结果=成功 逐字段:今日范围 10 行 / 2000-01-01 范围 0 行(时间过滤生效) |
|||
PASS 登录日志「退出」行(内联埋点 + token 重铸) | 退出埋点命中:opType=退出,operatorName=罗伟健,status/result=成功,ip=116.7.105.4,opTime=2026-09-09 22:13:14 (token |
|||
PASS 批量日志「导出」行(result+fileId+ZIP 魔数) | 导出留档行命中:page=线索设置,result=成功,countDetail=成功导出 1 条公海池,fileId=753015119432122368 (导出字节 4047,ZIP 魔数 ✓) |
|||
PASS 批量日志「导入」行(BatchResult 计数) | 导入行命中:result=成功,countDetail=成功导入 1,导入失败 0,fileId=(无失败明细→无错误报告文件,合理) BatchResult: total=1,success=1,fail=0 |
|||
PASS 结果文件下载 /api/log/batch/file(ZIP 魔数) | 结果文件下载成功:日志行 id=753015119490842624(fileId=753015119432122368),4047 字节,ZIP 魔数 PK ✓(90 天内有效) |
|||
PASS 打包下载 /api/log/batch/download ids[](ZIP 魔数) | 打包下载成功:ids=[753015119490842624, 753015121080483840],3506 字节,ZIP 魔数 PK ✓ |
|||
PASS 不存在文件 id 错误形态(40401/66001/404) | 不存在文件 id 错误形态生效:code=40401,批量操作日志不存在: 999999999999 (查无日志行→40401/HTTP404;过期→66001;行存在但无文件→66002;90 天分 |
|||
PASS BUDDY 三 page 权限拒绝(ADMIN 门禁) | 非管理员权限口径生效(ADMIN 门禁): page=拒绝 ✓(HTTP 403) page=拒绝 ✓(HTTP 403) page=拒绝 ✓(HTTP 403) |
|||
PASS 公海规则发布(部门专用)+ boundOppCount 基线 | 规则就绪:id=753015126608576512,name=e2d-绑定数规则-221152,scope=2(部门专用),deptIds=[744841292348915712] boundOppCount 基线 base=11571(本轮新建发布) 后续 |
|||
PASS 商机 A → boundOppCount=base+1(match 语义) | 商机 A(id=753015130098237440)→ boundOppCount=11572(base+1 ✓,match 语义生效) |
|||
PASS 商机 B → boundOppCount=base+2(动态统计非快照) | 商机 B(id=753015131109064704)→ boundOppCount=11573(base+2 ✓,动态统计非快照) |
|||
PASS 默认规则兜底口径(无默认规则降级提示) | 当前无发布中默认规则 → 兜底口径断言降级为提示(口径已由 crm-rule 单测覆盖:默认计数=不命中发布中专用规则的商机数,排除集=发布中专用规则部门并集) |
|||
PASS 清理(商机 close 正门 + 规则停用) | 清理完成:关停演示商机 2/2(close 终态;已关闭仍计数,仅 opp_status=5 排除——增量口径跨轮安全);演示规则已停用(发布行留存可辨) |
|||
PASS 网络层零旧契约形态 | bad_method=[] old_style=[] (共84请求) |
|||
PASS console/page error 双零(预期 403 拒绝豁免) | raw_console=3 exempt_403=3 real=0 page_errors=0 |
|||
|
|||
== SUMMARY == |
|||
PASS 登录 debug/token + /auth/me |
|||
PASS 同步任务 launch(任务编号格式 + 61023 两态容忍) | 任务化契约验证:taskId=753014778993049600,taskNo=SYNC-0909-02(SYNC-MMdd-序号 ✓) 异步终态:running(75s 未终态,任务行在位即可) |
|||
PASS 同步记录分页 + 开始时间倒序 | rows=2 desc=True first=2026-09-09 02:00:00 |
|||
PASS 任务详情五块结构(含 failDetails 失败明细) | 详情五块结构在位:taskNo=SYNC-0909-02,status=同步中,进度=5%,失败明细 0 条 |
|||
PASS 同步设置回显(派生 plan/nextExecuteTime/lastModifierName) | 设置回显:autoSyncEnabled=true,dailyTime=02:00,retryIntervalMinutes=10,retryMaxTimes=2,concurrency=1 派生:当前计划=每日 02:00(运行中),下次 |
|||
PASS 设置保存回显(interval=2 次数=3 并发=1) | 合法设置保存并回显:interval=2,次数=3,并发=1(autoSyncEnabled/dailyTime 保留原值不污染调度) |
|||
PASS 设置边界 61022 双路(间隔<1min / 次数<1 拒) | 61022 边界校验两路生效:间隔<1min 拒(重试间隔不能小于 1 分钟);次数<1 拒(重试次数不能小于 1 次) |
|||
PASS 设置还原(不污染调度原值) | 设置已还原(interval=10,次数=2,并发=1,dailyTime=02:00) |
|||
PASS lastSyncTime 切表口径 + 下次执行时间推导 | lastSyncTime 验证:与最新成功任务行 endTime 一致(2026-09-08 17:18:35)——切表生效 下次执行时间推导:autoSyncEnabled=true,nextExecuteTime=2026-09-10 02:00:00(推 |
|||
PASS AOP 写端点采集素材(建/删演示角色) | 写操作完成:建/删演示角色 e2d-日志演示角色-221152(等待采集落库后断言) |
|||
PASS 操作日志分页(新增/删除行命中 + 时间范围逐字段) | AOP 采集行命中:新增行 content=/api/roles/saveOrUpdate,操作人=罗伟健,结果=成功 逐字段:今日范围 10 行 / 2000-01-01 范围 0 行(时间过滤生效) |
|||
PASS 登录日志「退出」行(内联埋点 + token 重铸) | 退出埋点命中:opType=退出,operatorName=罗伟健,status/result=成功,ip=116.7.105.4,opTime=2026-09-09 22:13:14 (token |
|||
PASS 批量日志「导出」行(result+fileId+ZIP 魔数) | 导出留档行命中:page=线索设置,result=成功,countDetail=成功导出 1 条公海池,fileId=753015119432122368 (导出字节 4047,ZIP 魔数 ✓) |
|||
PASS 批量日志「导入」行(BatchResult 计数) | 导入行命中:result=成功,countDetail=成功导入 1,导入失败 0,fileId=(无失败明细→无错误报告文件,合理) BatchResult: total=1,success=1,fail=0 |
|||
PASS 结果文件下载 /api/log/batch/file(ZIP 魔数) | 结果文件下载成功:日志行 id=753015119490842624(fileId=753015119432122368),4047 字节,ZIP 魔数 PK ✓(90 天内有效) |
|||
PASS 打包下载 /api/log/batch/download ids[](ZIP 魔数) | 打包下载成功:ids=[753015119490842624, 753015121080483840],3506 字节,ZIP 魔数 PK ✓ |
|||
PASS 不存在文件 id 错误形态(40401/66001/404) | 不存在文件 id 错误形态生效:code=40401,批量操作日志不存在: 999999999999 (查无日志行→40401/HTTP404;过期→66001;行存在但无文件→66002;90 天分 |
|||
PASS BUDDY 三 page 权限拒绝(ADMIN 门禁) | 非管理员权限口径生效(ADMIN 门禁): page=拒绝 ✓(HTTP 403) page=拒绝 ✓(HTTP 403) page=拒绝 ✓(HTTP 403) |
|||
PASS 公海规则发布(部门专用)+ boundOppCount 基线 | 规则就绪:id=753015126608576512,name=e2d-绑定数规则-221152,scope=2(部门专用),deptIds=[744841292348915712] boundOppCount 基线 base=11571(本轮新建发布) 后续 |
|||
PASS 商机 A → boundOppCount=base+1(match 语义) | 商机 A(id=753015130098237440)→ boundOppCount=11572(base+1 ✓,match 语义生效) |
|||
PASS 商机 B → boundOppCount=base+2(动态统计非快照) | 商机 B(id=753015131109064704)→ boundOppCount=11573(base+2 ✓,动态统计非快照) |
|||
PASS 默认规则兜底口径(无默认规则降级提示) | 当前无发布中默认规则 → 兜底口径断言降级为提示(口径已由 crm-rule 单测覆盖:默认计数=不命中发布中专用规则的商机数,排除集=发布中专用规则部门并集) |
|||
PASS 清理(商机 close 正门 + 规则停用) | 清理完成:关停演示商机 2/2(close 终态;已关闭仍计数,仅 opp_status=5 排除——增量口径跨轮安全);演示规则已停用(发布行留存可辨) |
|||
PASS 网络层零旧契约形态 | bad_method=[] old_style=[] (共84请求) |
|||
PASS console/page error 双零(预期 403 拒绝豁免) | raw_console=3 exempt_403=3 real=0 page_errors=0 |
|||
console_errors: 3 (exempt_403: 3 ) page_errors: 0 |
|||
ALL_PASS: True |
|||
@ -0,0 +1,121 @@ |
|||
pre_clean: {'roles': 1, 'resources': 0, 'push_drafts': 0, 'dedup_drafts': 0, 'pools': 1, 'buddy_restored': True, 'dict_items': 0, 'dict_groups': 0} |
|||
PASS 登录 debug/token + /auth/me |
|||
PASS 字典分组列表预加载 dict/group/page(移植回归) |
|||
PASS 字典项列表预加载 dict/item/page(移植回归) |
|||
PASS v3 建演示分组A | e2d_group_a_220633 |
|||
PASS v3 弹窗结构(无键值/无默认勾选/子项编码列) | 键值输入=True 默认勾选=True |
|||
PASS v3 新建项(无键值)编码自动生成 | code=dict_17c7eeff44464fce9e513a28aa468b59 |
|||
PASS v3 名称组内唯一 63007 | 失败: 字典名称[e2d-项1]在本分组内已存在 (code=63007) |
|||
PASS v3 复合编辑加子项(编码自动生成) | 子项code=dict_4a02d03a85cb46668dc978ebd8a6c609 |
|||
PASS v3 分组设默认(仅二级项可选 + 父名/子名回显) | 两级提示=True cell=e2d-项1 / e2d-子项1 |
|||
PASS v3 分组默认项禁停 63013(行停用路) |
|||
PASS v3 演示分组清理 | e2d_group_a_220633 left=0 [子项:0:success | 一级:0:success | 分组:0:success] |
|||
PASS 用户分页 users/page | rows=10 |
|||
PASS 用户统计出参(三时间戳字段在位) | 部门=814,在职=6157,离职=0,未分配=6150 lastSyncTime=2026-09-08 17:18:35,lastTransferTime=—,lastTrans |
|||
PASS 移交预览 transfer-preview | 移交预览就绪:线索=1,商机=11306,项目=0,客户=17 (快照已存,步骤4 返还后与此对照) |
|||
PASS 自移交拦截 61019 | 61019 自移交拦截生效:移交方与接收人不能相同 |
|||
PASS 移交执行 admin→BUDDY(清零断言) | 移交执行成功:admin 名下清零;BUDDY 收到 线索=1,商机=11306,项目=0,客户=17 snapshot transferTime=2026-09-09 22:06 |
|||
PASS 移交返还(BUDDY 净零 + admin 保底) | 返还成功:BUDDY 名下回落至移交前(线索=0,商机=0,项目=0,客户=0);admin 各域 ≥ 原快照 |
|||
PASS stats.lastTransferTime 非空 | 出参时间戳到位 lastTransferTime=2026-09-09 22:06:55,lastTransferUser=罗伟健 lastSyncTime=2026-09-08 |
|||
PASS 演示角色就绪 | 演示角色就绪:e2d-演示角色-220633(id=753013530726236160,status=enabled) |
|||
PASS roles/stats 四计数 | 角色统计:总数=2,启用=2,停用=0,零使用=1 |
|||
PASS assign-roles-batch 增量分配 | 批量增量分配生效:BUDDY roles 含演示角色(id=753013530726236160) |
|||
PASS roles/copy 停用草稿 _copy_ | 复制产出停用草稿:roleName=e2d-演示角色-220633-副本,roleCode=e2d_role_220633_copy_m8RtQb,status=disabled |
|||
PASS roles/enable 校验 61021 | 61021 启用校验生效:角色未授权任何菜单,不可启用;请先在角色管理中分配权限资源 |
|||
PASS 内置角色禁停保护 | 内置角色保护生效:内置角色不允许停用 (code=61012) |
|||
PASS BUDDY 角色还原 + 删演示角色 | BUDDY 角色已还原(基准=[无]);删除角色 2/2 个 |
|||
PASS 演示权限点 + 禁删链路就绪 | 演示权限点就绪:id=753013540255694848(父 menu=747858510854750208);禁删链路就绪:演示角色 RD=753013541044224000 |
|||
PASS resources/references 非空 | 引用清单非空:管理员(在职7) |
|||
PASS resources 停用→启用 roundtrip | 演示点 停用→启用 roundtrip 成功(幂等) |
|||
PASS resources 禁删两段 61009+61020 | 禁删两段生效:启用中删除=61009(仅停用状态的资源可删除,请先停用该节点);停用后删除=61020(资源被 1 个角色授权引用,禁止删除;请先在角色管理中解除引用,角色ID:[7530135410 |
|||
PASS 演示权限点清理 | 演示角色 RD 已删(级联清引用);演示权限点已停用并删除 |
|||
PASS 推送 R1 草稿(仅步骤①,嵌套绑定) | R1 草稿就绪:id=753013550833729536,编码=PUSH_RULE_01,版本=V1.0 |
|||
PASS D-01 无步骤④发布(回归) | D-01 修复验证:无步骤④发布成功(status=2 发布中) |
|||
PASS 推送 R2 草稿 + 详情回显 | R2 草稿 4 步骤回显一致:basic=方案评审,校验行=1,映射行=1,createMode=auto,applyScope=2 条件值=投标阶段 |
|||
PASS 推送 R2 发布 + versions | R2 发布成功:versions 1 行,当前 V1.0 status=2 发布中 |
|||
PASS 推送 64029 同名发布中唯一 | 64029 同名称发布中唯一生效:已存在同名称的发布中规则:e2d-推送R2-220633 |
|||
PASS 推送 64030 创建方式值域 | 64030 创建方式值域校验生效:项目创建方式非法(可选:自动创建/用户交互/关联已有项目) |
|||
PASS 推送 copy 新编码 V1.0 草稿 | 复制成功:新编码=PUSH_RULE_05,V1.0 草稿(id=753013561009111040) |
|||
PASS 推送 disable + 64027 仅草稿可删 | 停用成功 + 仅草稿可删拦截生效(64027 仅草稿版本可删除) |
|||
PASS 推送草稿清理 | 清理草稿 3/3(R1/R2 已发布转停用留存,e2d- 前缀可辨) |
|||
PASS 查重 D1 发布 + referenceCount 出参 | D1 发布成功:status=2,referenceCount=0(出参字段在位) |
|||
PASS 查重 64031 属地缺对象 | 64031 属地档校验生效:按项目属地范围须填写适用对象 |
|||
PASS 查重 64031 仅全部商机可设默认 | 64031 仅全部商机可设默认生效:仅全部商机范围的规则可设为默认 |
|||
PASS 引用计数草稿引用入账 | 引用计数入账:detail.referenceCount=1,versions V1.0 referenceCount=1(草稿引用也计入) |
|||
PASS 草稿不可被正式引用 64030 | 64030 草稿查重规则引用拦截生效:引用的项目查重规则版本非发布中,草稿不可被正式引用 |
|||
PASS 查重状态机 copy/删/停用/64033 | 复制(新编码 V1.0 草稿)→ 草稿可删 ✓;停用 D1 → 删除被拦(64033 仅草稿版本可删除) |
|||
PASS 查重草稿清理 | 清理推送/查重草稿 3/3,D1.referenceCount 回落=0 |
|||
PASS 项目规则 current 5 区块 | 项目规则聚合回显:stageAliases=0行,poolMembers=0行,conflictPull=对象,stageReminds=0行,bidConfigs=0行 |
|||
PASS 项目规则 分区保存 + 回显 | 阶段别名保存并回显:2 行,S1.alias=e2d别名一-220633 |
|||
PASS 项目规则 64036 比例校验 | 64036 比例校验生效:标底方认定比例须在 0-100 之间 |
|||
PASS 线索池建池(三类异人成员提交) | 演示池就绪:id=753013590625091584,名称=e2d-演示池-220633,部门=744636475731083264(三类成员已提交) |
|||
PASS 线索池三类成员回填 | 三类成员回填一致:负责人=罗伟健,协作人=1 人,部门成员=1 人(uid=744842317625556992) |
|||
PASS 线索池导出 xlsx(ZIP 魔数) | 导出成功:4196 字节,ZIP 魔数 PK ✓(xlsx) |
|||
PASS 线索池导入 BatchResult | 导入成功:BatchResult total=1,success=1,fail=0 |
|||
PASS 线索池列表可见(演示+导入) | 列表可见:演示池 + 导入池均在(共 e2d- 池 3 个) |
|||
PASS 字典种子 opp_stage 06/07 + opportunity_status | 字典种子在位:opp_stage 7 值含 06技术交流/07竞品打压;opportunity_status 4 值(推进中、暂缓中、已关闭、已转项目) |
|||
PASS 线索池清理 | 清理演示池/导入池 2/2 |
|||
PASS 网络层零旧契约形态 | bad_method=[] old_style=[] (共145请求) |
|||
PASS v3 saveOrUpdate 载荷零 value/isDefault | save提交=3次 bad=[] |
|||
|
|||
== SUMMARY == |
|||
PASS 登录 debug/token + /auth/me |
|||
PASS 字典分组列表预加载 dict/group/page(移植回归) |
|||
PASS 字典项列表预加载 dict/item/page(移植回归) |
|||
PASS v3 建演示分组A | e2d_group_a_220633 |
|||
PASS v3 弹窗结构(无键值/无默认勾选/子项编码列) | 键值输入=True 默认勾选=True |
|||
PASS v3 新建项(无键值)编码自动生成 | code=dict_17c7eeff44464fce9e513a28aa468b59 |
|||
PASS v3 名称组内唯一 63007 | 失败: 字典名称[e2d-项1]在本分组内已存在 (code=63007) |
|||
PASS v3 复合编辑加子项(编码自动生成) | 子项code=dict_4a02d03a85cb46668dc978ebd8a6c609 |
|||
PASS v3 分组设默认(仅二级项可选 + 父名/子名回显) | 两级提示=True cell=e2d-项1 / e2d-子项1 |
|||
PASS v3 分组默认项禁停 63013(行停用路) |
|||
PASS v3 演示分组清理 | e2d_group_a_220633 left=0 [子项:0:success | 一级:0:success | 分组:0:success] |
|||
PASS 用户分页 users/page | rows=10 |
|||
PASS 用户统计出参(三时间戳字段在位) | 部门=814,在职=6157,离职=0,未分配=6150 lastSyncTime=2026-09-08 17:18:35,lastTransferTime=—,lastTrans |
|||
PASS 移交预览 transfer-preview | 移交预览就绪:线索=1,商机=11306,项目=0,客户=17 (快照已存,步骤4 返还后与此对照) |
|||
PASS 自移交拦截 61019 | 61019 自移交拦截生效:移交方与接收人不能相同 |
|||
PASS 移交执行 admin→BUDDY(清零断言) | 移交执行成功:admin 名下清零;BUDDY 收到 线索=1,商机=11306,项目=0,客户=17 snapshot transferTime=2026-09-09 22:06 |
|||
PASS 移交返还(BUDDY 净零 + admin 保底) | 返还成功:BUDDY 名下回落至移交前(线索=0,商机=0,项目=0,客户=0);admin 各域 ≥ 原快照 |
|||
PASS stats.lastTransferTime 非空 | 出参时间戳到位 lastTransferTime=2026-09-09 22:06:55,lastTransferUser=罗伟健 lastSyncTime=2026-09-08 |
|||
PASS 演示角色就绪 | 演示角色就绪:e2d-演示角色-220633(id=753013530726236160,status=enabled) |
|||
PASS roles/stats 四计数 | 角色统计:总数=2,启用=2,停用=0,零使用=1 |
|||
PASS assign-roles-batch 增量分配 | 批量增量分配生效:BUDDY roles 含演示角色(id=753013530726236160) |
|||
PASS roles/copy 停用草稿 _copy_ | 复制产出停用草稿:roleName=e2d-演示角色-220633-副本,roleCode=e2d_role_220633_copy_m8RtQb,status=disabled |
|||
PASS roles/enable 校验 61021 | 61021 启用校验生效:角色未授权任何菜单,不可启用;请先在角色管理中分配权限资源 |
|||
PASS 内置角色禁停保护 | 内置角色保护生效:内置角色不允许停用 (code=61012) |
|||
PASS BUDDY 角色还原 + 删演示角色 | BUDDY 角色已还原(基准=[无]);删除角色 2/2 个 |
|||
PASS 演示权限点 + 禁删链路就绪 | 演示权限点就绪:id=753013540255694848(父 menu=747858510854750208);禁删链路就绪:演示角色 RD=753013541044224000 |
|||
PASS resources/references 非空 | 引用清单非空:管理员(在职7) |
|||
PASS resources 停用→启用 roundtrip | 演示点 停用→启用 roundtrip 成功(幂等) |
|||
PASS resources 禁删两段 61009+61020 | 禁删两段生效:启用中删除=61009(仅停用状态的资源可删除,请先停用该节点);停用后删除=61020(资源被 1 个角色授权引用,禁止删除;请先在角色管理中解除引用,角色ID:[7530135410 |
|||
PASS 演示权限点清理 | 演示角色 RD 已删(级联清引用);演示权限点已停用并删除 |
|||
PASS 推送 R1 草稿(仅步骤①,嵌套绑定) | R1 草稿就绪:id=753013550833729536,编码=PUSH_RULE_01,版本=V1.0 |
|||
PASS D-01 无步骤④发布(回归) | D-01 修复验证:无步骤④发布成功(status=2 发布中) |
|||
PASS 推送 R2 草稿 + 详情回显 | R2 草稿 4 步骤回显一致:basic=方案评审,校验行=1,映射行=1,createMode=auto,applyScope=2 条件值=投标阶段 |
|||
PASS 推送 R2 发布 + versions | R2 发布成功:versions 1 行,当前 V1.0 status=2 发布中 |
|||
PASS 推送 64029 同名发布中唯一 | 64029 同名称发布中唯一生效:已存在同名称的发布中规则:e2d-推送R2-220633 |
|||
PASS 推送 64030 创建方式值域 | 64030 创建方式值域校验生效:项目创建方式非法(可选:自动创建/用户交互/关联已有项目) |
|||
PASS 推送 copy 新编码 V1.0 草稿 | 复制成功:新编码=PUSH_RULE_05,V1.0 草稿(id=753013561009111040) |
|||
PASS 推送 disable + 64027 仅草稿可删 | 停用成功 + 仅草稿可删拦截生效(64027 仅草稿版本可删除) |
|||
PASS 推送草稿清理 | 清理草稿 3/3(R1/R2 已发布转停用留存,e2d- 前缀可辨) |
|||
PASS 查重 D1 发布 + referenceCount 出参 | D1 发布成功:status=2,referenceCount=0(出参字段在位) |
|||
PASS 查重 64031 属地缺对象 | 64031 属地档校验生效:按项目属地范围须填写适用对象 |
|||
PASS 查重 64031 仅全部商机可设默认 | 64031 仅全部商机可设默认生效:仅全部商机范围的规则可设为默认 |
|||
PASS 引用计数草稿引用入账 | 引用计数入账:detail.referenceCount=1,versions V1.0 referenceCount=1(草稿引用也计入) |
|||
PASS 草稿不可被正式引用 64030 | 64030 草稿查重规则引用拦截生效:引用的项目查重规则版本非发布中,草稿不可被正式引用 |
|||
PASS 查重状态机 copy/删/停用/64033 | 复制(新编码 V1.0 草稿)→ 草稿可删 ✓;停用 D1 → 删除被拦(64033 仅草稿版本可删除) |
|||
PASS 查重草稿清理 | 清理推送/查重草稿 3/3,D1.referenceCount 回落=0 |
|||
PASS 项目规则 current 5 区块 | 项目规则聚合回显:stageAliases=0行,poolMembers=0行,conflictPull=对象,stageReminds=0行,bidConfigs=0行 |
|||
PASS 项目规则 分区保存 + 回显 | 阶段别名保存并回显:2 行,S1.alias=e2d别名一-220633 |
|||
PASS 项目规则 64036 比例校验 | 64036 比例校验生效:标底方认定比例须在 0-100 之间 |
|||
PASS 线索池建池(三类异人成员提交) | 演示池就绪:id=753013590625091584,名称=e2d-演示池-220633,部门=744636475731083264(三类成员已提交) |
|||
PASS 线索池三类成员回填 | 三类成员回填一致:负责人=罗伟健,协作人=1 人,部门成员=1 人(uid=744842317625556992) |
|||
PASS 线索池导出 xlsx(ZIP 魔数) | 导出成功:4196 字节,ZIP 魔数 PK ✓(xlsx) |
|||
PASS 线索池导入 BatchResult | 导入成功:BatchResult total=1,success=1,fail=0 |
|||
PASS 线索池列表可见(演示+导入) | 列表可见:演示池 + 导入池均在(共 e2d- 池 3 个) |
|||
PASS 字典种子 opp_stage 06/07 + opportunity_status | 字典种子在位:opp_stage 7 值含 06技术交流/07竞品打压;opportunity_status 4 值(推进中、暂缓中、已关闭、已转项目) |
|||
PASS 线索池清理 | 清理演示池/导入池 2/2 |
|||
PASS 网络层零旧契约形态 | bad_method=[] old_style=[] (共145请求) |
|||
PASS v3 saveOrUpdate 载荷零 value/isDefault | save提交=3次 bad=[] |
|||
console_errors: 0 page_errors: 0 |
|||
ALL_PASS: True |
|||
@ -0,0 +1,121 @@ |
|||
pre_clean: {'roles': 0, 'resources': 0, 'push_drafts': 0, 'dedup_drafts': 0, 'pools': 1, 'buddy_restored': True, 'dict_items': 0, 'dict_groups': 0} |
|||
PASS 登录 debug/token + /auth/me |
|||
PASS 字典分组列表预加载 dict/group/page(移植回归) |
|||
PASS 字典项列表预加载 dict/item/page(移植回归) |
|||
PASS v3 建演示分组A | e2d_group_a_221451 |
|||
PASS v3 弹窗结构(无键值/无默认勾选/子项编码列) | 键值输入=True 默认勾选=True |
|||
PASS v3 新建项(无键值)编码自动生成 | code=dict_c33ce3ff00d646b3bd054961f3e5362e |
|||
PASS v3 名称组内唯一 63007 | 失败: 字典名称[e2d-项1]在本分组内已存在 (code=63007) |
|||
PASS v3 复合编辑加子项(编码自动生成) | 子项code=dict_65ce74e1083948a4b33c06a6e46cdaca |
|||
PASS v3 分组设默认(仅二级项可选 + 父名/子名回显) | 两级提示=True cell=e2d-项1 / e2d-子项1 |
|||
PASS v3 分组默认项禁停 63013(行停用路) |
|||
PASS v3 演示分组清理 | e2d_group_a_221451 left=0 [子项:0:success | 一级:0:success | 分组:0:success] |
|||
PASS 用户分页 users/page | rows=10 |
|||
PASS 用户统计出参(三时间戳字段在位) | 部门=814,在职=6157,离职=0,未分配=6150 lastSyncTime=2026-09-08 17:18:35,lastTransferTime=2026-09-09 |
|||
PASS 移交预览 transfer-preview | 移交预览就绪:线索=1,商机=11308,项目=0,客户=17 (快照已存,步骤4 返还后与此对照) |
|||
PASS 自移交拦截 61019 | 61019 自移交拦截生效:移交方与接收人不能相同 |
|||
PASS 移交执行 admin→BUDDY(清零断言) | 移交执行成功:admin 名下清零;BUDDY 收到 线索=1,商机=11308,项目=0,客户=17 snapshot transferTime=2026-09-09 22:15 |
|||
PASS 移交返还(BUDDY 净零 + admin 保底) | 返还成功:BUDDY 名下回落至移交前(线索=0,商机=0,项目=0,客户=0);admin 各域 ≥ 原快照 |
|||
PASS stats.lastTransferTime 非空 | 出参时间戳到位 lastTransferTime=2026-09-09 22:15:13,lastTransferUser=曾偲青 lastSyncTime=2026-09-08 |
|||
PASS 演示角色就绪 | 演示角色就绪:e2d-演示角色-221451(id=753015620693393408,status=enabled) |
|||
PASS roles/stats 四计数 | 角色统计:总数=2,启用=2,停用=0,零使用=1 |
|||
PASS assign-roles-batch 增量分配 | 批量增量分配生效:BUDDY roles 含演示角色(id=753015620693393408) |
|||
PASS roles/copy 停用草稿 _copy_ | 复制产出停用草稿:roleName=e2d-演示角色-221451-副本,roleCode=e2d_role_221451_copy_ApUCLl,status=disabled |
|||
PASS roles/enable 校验 61021 | 61021 启用校验生效:角色未授权任何菜单,不可启用;请先在角色管理中分配权限资源 |
|||
PASS 内置角色禁停保护 | 内置角色保护生效:内置角色不允许停用 (code=61012) |
|||
PASS BUDDY 角色还原 + 删演示角色 | BUDDY 角色已还原(基准=[无]);删除角色 2/2 个 |
|||
PASS 演示权限点 + 禁删链路就绪 | 演示权限点就绪:id=753015629547569152(父 menu=747858510854750208);禁删链路就绪:演示角色 RD=753015630210269184 |
|||
PASS resources/references 非空 | 引用清单非空:管理员(在职7) |
|||
PASS resources 停用→启用 roundtrip | 演示点 停用→启用 roundtrip 成功(幂等) |
|||
PASS resources 禁删两段 61009+61020 | 禁删两段生效:启用中删除=61009(仅停用状态的资源可删除,请先停用该节点);停用后删除=61020(资源被 1 个角色授权引用,禁止删除;请先在角色管理中解除引用,角色ID:[7530156302 |
|||
PASS 演示权限点清理 | 演示角色 RD 已删(级联清引用);演示权限点已停用并删除 |
|||
PASS 推送 R1 草稿(仅步骤①,嵌套绑定) | R1 草稿就绪:id=753015640054300672,编码=PUSH_RULE_03,版本=V1.0 |
|||
PASS D-01 无步骤④发布(回归) | D-01 修复验证:无步骤④发布成功(status=2 发布中) |
|||
PASS 推送 R2 草稿 + 详情回显 | R2 草稿 4 步骤回显一致:basic=方案评审,校验行=1,映射行=1,createMode=auto,applyScope=2 条件值=投标阶段 |
|||
PASS 推送 R2 发布 + versions | R2 发布成功:versions 1 行,当前 V1.0 status=2 发布中 |
|||
PASS 推送 64029 同名发布中唯一 | 64029 同名称发布中唯一生效:已存在同名称的发布中规则:e2d-推送R2-221451 |
|||
PASS 推送 64030 创建方式值域 | 64030 创建方式值域校验生效:项目创建方式非法(可选:自动创建/用户交互/关联已有项目) |
|||
PASS 推送 copy 新编码 V1.0 草稿 | 复制成功:新编码=PUSH_RULE_07,V1.0 草稿(id=753015647998312448) |
|||
PASS 推送 disable + 64027 仅草稿可删 | 停用成功 + 仅草稿可删拦截生效(64027 仅草稿版本可删除) |
|||
PASS 推送草稿清理 | 清理草稿 3/3(R1/R2 已发布转停用留存,e2d- 前缀可辨) |
|||
PASS 查重 D1 发布 + referenceCount 出参 | D1 发布成功:status=2,referenceCount=0(出参字段在位) |
|||
PASS 查重 64031 属地缺对象 | 64031 属地档校验生效:按项目属地范围须填写适用对象 |
|||
PASS 查重 64031 仅全部商机可设默认 | 64031 仅全部商机可设默认生效:仅全部商机范围的规则可设为默认 |
|||
PASS 引用计数草稿引用入账 | 引用计数入账:detail.referenceCount=1,versions V1.0 referenceCount=1(草稿引用也计入) |
|||
PASS 草稿不可被正式引用 64030 | 64030 草稿查重规则引用拦截生效:引用的项目查重规则版本非发布中,草稿不可被正式引用 |
|||
PASS 查重状态机 copy/删/停用/64033 | 复制(新编码 V1.0 草稿)→ 草稿可删 ✓;停用 D1 → 删除被拦(64033 仅草稿版本可删除) |
|||
PASS 查重草稿清理 | 清理推送/查重草稿 3/3,D1.referenceCount 回落=0 |
|||
PASS 项目规则 current 5 区块 | 项目规则聚合回显:stageAliases=2行,poolMembers=0行,conflictPull=对象,stageReminds=0行,bidConfigs=0行 |
|||
PASS 项目规则 分区保存 + 回显 | 阶段别名保存并回显:2 行,S1.alias=e2d别名一-221451 |
|||
PASS 项目规则 64036 比例校验 | 64036 比例校验生效:标底方认定比例须在 0-100 之间 |
|||
PASS 线索池建池(三类异人成员提交) | 演示池就绪:id=753015674674085888,名称=e2d-演示池-221451,部门=744636475731083264(三类成员已提交) |
|||
PASS 线索池三类成员回填 | 三类成员回填一致:负责人=罗伟健,协作人=1 人,部门成员=1 人(uid=744842317625556992) |
|||
PASS 线索池导出 xlsx(ZIP 魔数) | 导出成功:4202 字节,ZIP 魔数 PK ✓(xlsx) |
|||
PASS 线索池导入 BatchResult | 导入成功:BatchResult total=1,success=1,fail=0 |
|||
PASS 线索池列表可见(演示+导入) | 列表可见:演示池 + 导入池均在(共 e2d- 池 3 个) |
|||
PASS 字典种子 opp_stage 06/07 + opportunity_status | 字典种子在位:opp_stage 7 值含 06技术交流/07竞品打压;opportunity_status 4 值(推进中、暂缓中、已关闭、已转项目) |
|||
PASS 线索池清理 | 清理演示池/导入池 2/2 |
|||
PASS 网络层零旧契约形态 | bad_method=[] old_style=[] (共145请求) |
|||
PASS v3 saveOrUpdate 载荷零 value/isDefault | save提交=3次 bad=[] |
|||
|
|||
== SUMMARY == |
|||
PASS 登录 debug/token + /auth/me |
|||
PASS 字典分组列表预加载 dict/group/page(移植回归) |
|||
PASS 字典项列表预加载 dict/item/page(移植回归) |
|||
PASS v3 建演示分组A | e2d_group_a_221451 |
|||
PASS v3 弹窗结构(无键值/无默认勾选/子项编码列) | 键值输入=True 默认勾选=True |
|||
PASS v3 新建项(无键值)编码自动生成 | code=dict_c33ce3ff00d646b3bd054961f3e5362e |
|||
PASS v3 名称组内唯一 63007 | 失败: 字典名称[e2d-项1]在本分组内已存在 (code=63007) |
|||
PASS v3 复合编辑加子项(编码自动生成) | 子项code=dict_65ce74e1083948a4b33c06a6e46cdaca |
|||
PASS v3 分组设默认(仅二级项可选 + 父名/子名回显) | 两级提示=True cell=e2d-项1 / e2d-子项1 |
|||
PASS v3 分组默认项禁停 63013(行停用路) |
|||
PASS v3 演示分组清理 | e2d_group_a_221451 left=0 [子项:0:success | 一级:0:success | 分组:0:success] |
|||
PASS 用户分页 users/page | rows=10 |
|||
PASS 用户统计出参(三时间戳字段在位) | 部门=814,在职=6157,离职=0,未分配=6150 lastSyncTime=2026-09-08 17:18:35,lastTransferTime=2026-09-09 |
|||
PASS 移交预览 transfer-preview | 移交预览就绪:线索=1,商机=11308,项目=0,客户=17 (快照已存,步骤4 返还后与此对照) |
|||
PASS 自移交拦截 61019 | 61019 自移交拦截生效:移交方与接收人不能相同 |
|||
PASS 移交执行 admin→BUDDY(清零断言) | 移交执行成功:admin 名下清零;BUDDY 收到 线索=1,商机=11308,项目=0,客户=17 snapshot transferTime=2026-09-09 22:15 |
|||
PASS 移交返还(BUDDY 净零 + admin 保底) | 返还成功:BUDDY 名下回落至移交前(线索=0,商机=0,项目=0,客户=0);admin 各域 ≥ 原快照 |
|||
PASS stats.lastTransferTime 非空 | 出参时间戳到位 lastTransferTime=2026-09-09 22:15:13,lastTransferUser=曾偲青 lastSyncTime=2026-09-08 |
|||
PASS 演示角色就绪 | 演示角色就绪:e2d-演示角色-221451(id=753015620693393408,status=enabled) |
|||
PASS roles/stats 四计数 | 角色统计:总数=2,启用=2,停用=0,零使用=1 |
|||
PASS assign-roles-batch 增量分配 | 批量增量分配生效:BUDDY roles 含演示角色(id=753015620693393408) |
|||
PASS roles/copy 停用草稿 _copy_ | 复制产出停用草稿:roleName=e2d-演示角色-221451-副本,roleCode=e2d_role_221451_copy_ApUCLl,status=disabled |
|||
PASS roles/enable 校验 61021 | 61021 启用校验生效:角色未授权任何菜单,不可启用;请先在角色管理中分配权限资源 |
|||
PASS 内置角色禁停保护 | 内置角色保护生效:内置角色不允许停用 (code=61012) |
|||
PASS BUDDY 角色还原 + 删演示角色 | BUDDY 角色已还原(基准=[无]);删除角色 2/2 个 |
|||
PASS 演示权限点 + 禁删链路就绪 | 演示权限点就绪:id=753015629547569152(父 menu=747858510854750208);禁删链路就绪:演示角色 RD=753015630210269184 |
|||
PASS resources/references 非空 | 引用清单非空:管理员(在职7) |
|||
PASS resources 停用→启用 roundtrip | 演示点 停用→启用 roundtrip 成功(幂等) |
|||
PASS resources 禁删两段 61009+61020 | 禁删两段生效:启用中删除=61009(仅停用状态的资源可删除,请先停用该节点);停用后删除=61020(资源被 1 个角色授权引用,禁止删除;请先在角色管理中解除引用,角色ID:[7530156302 |
|||
PASS 演示权限点清理 | 演示角色 RD 已删(级联清引用);演示权限点已停用并删除 |
|||
PASS 推送 R1 草稿(仅步骤①,嵌套绑定) | R1 草稿就绪:id=753015640054300672,编码=PUSH_RULE_03,版本=V1.0 |
|||
PASS D-01 无步骤④发布(回归) | D-01 修复验证:无步骤④发布成功(status=2 发布中) |
|||
PASS 推送 R2 草稿 + 详情回显 | R2 草稿 4 步骤回显一致:basic=方案评审,校验行=1,映射行=1,createMode=auto,applyScope=2 条件值=投标阶段 |
|||
PASS 推送 R2 发布 + versions | R2 发布成功:versions 1 行,当前 V1.0 status=2 发布中 |
|||
PASS 推送 64029 同名发布中唯一 | 64029 同名称发布中唯一生效:已存在同名称的发布中规则:e2d-推送R2-221451 |
|||
PASS 推送 64030 创建方式值域 | 64030 创建方式值域校验生效:项目创建方式非法(可选:自动创建/用户交互/关联已有项目) |
|||
PASS 推送 copy 新编码 V1.0 草稿 | 复制成功:新编码=PUSH_RULE_07,V1.0 草稿(id=753015647998312448) |
|||
PASS 推送 disable + 64027 仅草稿可删 | 停用成功 + 仅草稿可删拦截生效(64027 仅草稿版本可删除) |
|||
PASS 推送草稿清理 | 清理草稿 3/3(R1/R2 已发布转停用留存,e2d- 前缀可辨) |
|||
PASS 查重 D1 发布 + referenceCount 出参 | D1 发布成功:status=2,referenceCount=0(出参字段在位) |
|||
PASS 查重 64031 属地缺对象 | 64031 属地档校验生效:按项目属地范围须填写适用对象 |
|||
PASS 查重 64031 仅全部商机可设默认 | 64031 仅全部商机可设默认生效:仅全部商机范围的规则可设为默认 |
|||
PASS 引用计数草稿引用入账 | 引用计数入账:detail.referenceCount=1,versions V1.0 referenceCount=1(草稿引用也计入) |
|||
PASS 草稿不可被正式引用 64030 | 64030 草稿查重规则引用拦截生效:引用的项目查重规则版本非发布中,草稿不可被正式引用 |
|||
PASS 查重状态机 copy/删/停用/64033 | 复制(新编码 V1.0 草稿)→ 草稿可删 ✓;停用 D1 → 删除被拦(64033 仅草稿版本可删除) |
|||
PASS 查重草稿清理 | 清理推送/查重草稿 3/3,D1.referenceCount 回落=0 |
|||
PASS 项目规则 current 5 区块 | 项目规则聚合回显:stageAliases=2行,poolMembers=0行,conflictPull=对象,stageReminds=0行,bidConfigs=0行 |
|||
PASS 项目规则 分区保存 + 回显 | 阶段别名保存并回显:2 行,S1.alias=e2d别名一-221451 |
|||
PASS 项目规则 64036 比例校验 | 64036 比例校验生效:标底方认定比例须在 0-100 之间 |
|||
PASS 线索池建池(三类异人成员提交) | 演示池就绪:id=753015674674085888,名称=e2d-演示池-221451,部门=744636475731083264(三类成员已提交) |
|||
PASS 线索池三类成员回填 | 三类成员回填一致:负责人=罗伟健,协作人=1 人,部门成员=1 人(uid=744842317625556992) |
|||
PASS 线索池导出 xlsx(ZIP 魔数) | 导出成功:4202 字节,ZIP 魔数 PK ✓(xlsx) |
|||
PASS 线索池导入 BatchResult | 导入成功:BatchResult total=1,success=1,fail=0 |
|||
PASS 线索池列表可见(演示+导入) | 列表可见:演示池 + 导入池均在(共 e2d- 池 3 个) |
|||
PASS 字典种子 opp_stage 06/07 + opportunity_status | 字典种子在位:opp_stage 7 值含 06技术交流/07竞品打压;opportunity_status 4 值(推进中、暂缓中、已关闭、已转项目) |
|||
PASS 线索池清理 | 清理演示池/导入池 2/2 |
|||
PASS 网络层零旧契约形态 | bad_method=[] old_style=[] (共145请求) |
|||
PASS v3 saveOrUpdate 载荷零 value/isDefault | save提交=3次 bad=[] |
|||
console_errors: 0 page_errors: 0 |
|||
ALL_PASS: True |
|||