Browse Source

commit

master^2
luoweijian 3 hours ago
parent
commit
3ba6659d1e
  1. 78
      .scratch/a7-acceptance/README.md
  2. 35
      .scratch/a7-acceptance/_check_demo.py
  3. 91
      .scratch/a7-acceptance/_gen_checklist.py
  4. 112
      .scratch/a7-acceptance/_merge_demo.py
  5. 2190
      .scratch/a7-acceptance/demo/index.html
  6. 143
      .scratch/a7-acceptance/heavy-smoke-result.json
  7. 19
      .scratch/a7-acceptance/issues/_TEMPLATE.md
  8. 298
      .scratch/a7-acceptance/parity-smoke-result.json
  9. BIN
      .scratch/a7-acceptance/shots-heavy-run/00-open.png
  10. BIN
      .scratch/a7-acceptance/shots-heavy-run/01-sync-launch.png
  11. BIN
      .scratch/a7-acceptance/shots-heavy-run/01-user-stats.png
  12. BIN
      .scratch/a7-acceptance/shots-heavy-run/02-sync-settings.png
  13. BIN
      .scratch/a7-acceptance/shots-heavy-run/02-transfer.png
  14. BIN
      .scratch/a7-acceptance/shots-heavy-run/03-role-batch.png
  15. BIN
      .scratch/a7-acceptance/shots-heavy-run/03-sync-stats.png
  16. BIN
      .scratch/a7-acceptance/shots-heavy-run/04-glog-prepare.png
  17. BIN
      .scratch/a7-acceptance/shots-heavy-run/04-resource-blocked.png
  18. BIN
      .scratch/a7-acceptance/shots-heavy-run/05-glog-export.png
  19. BIN
      .scratch/a7-acceptance/shots-heavy-run/05-push-d01.png
  20. BIN
      .scratch/a7-acceptance/shots-heavy-run/06-glog-denied.png
  21. BIN
      .scratch/a7-acceptance/shots-heavy-run/06-push-disabled.png
  22. BIN
      .scratch/a7-acceptance/shots-heavy-run/07-dedup-refcount.png
  23. BIN
      .scratch/a7-acceptance/shots-heavy-run/07-poolrule-base.png
  24. BIN
      .scratch/a7-acceptance/shots-heavy-run/08-cleanup.png
  25. BIN
      .scratch/a7-acceptance/shots-heavy-run/08-pool-members.png
  26. BIN
      .scratch/a7-acceptance/shots-heavy-run/09-dict-seed.png
  27. BIN
      .scratch/a7-acceptance/shots-parity/00-open.png
  28. BIN
      .scratch/a7-acceptance/shots-parity/01-user-stats.png
  29. BIN
      .scratch/a7-acceptance/shots-parity/02-transfer.png
  30. BIN
      .scratch/a7-acceptance/shots-parity/03-role-batch.png
  31. BIN
      .scratch/a7-acceptance/shots-parity/04-resource-blocked.png
  32. BIN
      .scratch/a7-acceptance/shots-parity/05-push-d01.png
  33. BIN
      .scratch/a7-acceptance/shots-parity/06-push-disabled.png
  34. BIN
      .scratch/a7-acceptance/shots-parity/07-dedup-refcount.png
  35. BIN
      .scratch/a7-acceptance/shots-parity/08-pool-members.png
  36. BIN
      .scratch/a7-acceptance/shots-parity/09-dict-seed.png
  37. 245
      .scratch/a7-acceptance/验收步骤清单.md
  38. 53
      .scratch/a7-acceptance/验收结果汇总.md
  39. 57
      .scratch/a7-heavy/demo-smoke-result.json
  40. 3
      .scratch/a7-heavy/demo/index.html
  41. 10
      .scratch/a7-heavy/demo_smoke.py
  42. 32
      .scratch/a7-heavy/issues/04-bruno-regen-baseline.md
  43. 28
      .scratch/a7-heavy/issues/05-e2e-demo-smoke.md
  44. 3
      .scratch/a7-heavy/map.md
  45. 84
      .scratch/a7-heavy/probe_once.py
  46. BIN
      .scratch/a7-heavy/shots/03-sync-stats.png
  47. BIN
      .scratch/a7-heavy/shots/04-glog-prepare.png
  48. BIN
      .scratch/a7-heavy/shots/05-glog-export.png
  49. BIN
      .scratch/a7-heavy/shots/06-glog-denied.png
  50. BIN
      .scratch/a7-heavy/shots/07-poolrule-base.png
  51. BIN
      .scratch/a7-heavy/shots/08-cleanup.png
  52. 10
      .scratch/a7-parity/demo_smoke.py
  53. 1
      .scratch/a7-parity/gap-report.md
  54. 4
      .scratch/a7-parity/map.md
  55. 13
      .scratch/customer-contact-graph/issues/00-prototype-demo.md
  56. 1243
      .scratch/customer-contact-graph/prototype/prototype-contact-graph.html
  57. 180
      .scratch/customer-contact-graph/prototype/selfcheck-result.json
  58. 235
      .scratch/customer-contact-graph/prototype/selfcheck.py
  59. BIN
      .scratch/customer-contact-graph/prototype/shots/01-list.png
  60. BIN
      .scratch/customer-contact-graph/prototype/shots/02-reveal.png
  61. BIN
      .scratch/customer-contact-graph/prototype/shots/03-graph.png
  62. BIN
      .scratch/customer-contact-graph/prototype/shots/05-cycle.png
  63. BIN
      .scratch/customer-contact-graph/prototype/shots/05b-level.png
  64. BIN
      .scratch/customer-contact-graph/prototype/shots/06-qa-added.png
  65. BIN
      .scratch/customer-contact-graph/prototype/shots/07-jobchange-graph.png
  66. BIN
      .scratch/customer-contact-graph/prototype/shots/08-veto.png
  67. BIN
      .scratch/customer-contact-graph/prototype/shots/09-realdrag.png
  68. 176
      .scratch/customer-rework/bruno-recon-d.json
  69. 2
      .scratch/customer-rework/bruno_scan_d.py
  70. 55
      logs/a7-acc-heavy.log
  71. 121
      logs/a7-acc-parity.log
  72. 121
      logs/a7-acc-parity2.log
  73. 1431
      logs/crm-app.log
  74. 1643
      logs/mvn-pkg-a7h05.log
  75. 3732
      logs/mvn-test-a7h04.log

78
.scratch/a7-acceptance/README.md

@ -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 按缺陷块逐条定位修复;争议留白项等负责人确认后再改版。

35
.scratch/a7-acceptance/_check_demo.py

@ -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))

91
.scratch/a7-acceptance/_gen_checklist.py

@ -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)))

112
.scratch/a7-acceptance/_merge_demo.py

@ -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)))

2190
.scratch/a7-acceptance/demo/index.html

File diff suppressed because it is too large

143
.scratch/a7-acceptance/heavy-smoke-result.json

@ -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
}

19
.scratch/a7-acceptance/issues/_TEMPLATE.md

@ -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

298
.scratch/a7-acceptance/parity-smoke-result.json

@ -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
}

BIN
.scratch/a7-acceptance/shots-heavy-run/00-open.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
.scratch/a7-acceptance/shots-heavy-run/01-sync-launch.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
.scratch/a7-acceptance/shots-heavy-run/01-user-stats.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
.scratch/a7-acceptance/shots-heavy-run/02-sync-settings.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
.scratch/a7-acceptance/shots-heavy-run/02-transfer.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
.scratch/a7-acceptance/shots-heavy-run/03-role-batch.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
.scratch/a7-acceptance/shots-heavy-run/03-sync-stats.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
.scratch/a7-acceptance/shots-heavy-run/04-glog-prepare.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
.scratch/a7-acceptance/shots-heavy-run/04-resource-blocked.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
.scratch/a7-acceptance/shots-heavy-run/05-glog-export.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
.scratch/a7-acceptance/shots-heavy-run/05-push-d01.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
.scratch/a7-acceptance/shots-heavy-run/06-glog-denied.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
.scratch/a7-acceptance/shots-heavy-run/06-push-disabled.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
.scratch/a7-acceptance/shots-heavy-run/07-dedup-refcount.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
.scratch/a7-acceptance/shots-heavy-run/07-poolrule-base.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
.scratch/a7-acceptance/shots-heavy-run/08-cleanup.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

BIN
.scratch/a7-acceptance/shots-heavy-run/08-pool-members.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
.scratch/a7-acceptance/shots-heavy-run/09-dict-seed.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
.scratch/a7-acceptance/shots-parity/00-open.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
.scratch/a7-acceptance/shots-parity/01-user-stats.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
.scratch/a7-acceptance/shots-parity/02-transfer.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
.scratch/a7-acceptance/shots-parity/03-role-batch.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
.scratch/a7-acceptance/shots-parity/04-resource-blocked.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
.scratch/a7-acceptance/shots-parity/05-push-d01.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
.scratch/a7-acceptance/shots-parity/06-push-disabled.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
.scratch/a7-acceptance/shots-parity/07-dedup-refcount.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
.scratch/a7-acceptance/shots-parity/08-pool-members.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
.scratch/a7-acceptance/shots-parity/09-dict-seed.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

245
.scratch/a7-acceptance/验收步骤清单.md

@ -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- 前缀 + 步骤面板末步清理。

53
.scratch/a7-acceptance/验收结果汇总.md

@ -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。

57
.scratch/a7-heavy/demo-smoke-result.json

@ -1,13 +1,17 @@
{
"ts": "172728",
"ts": "214637",
"console_errors": [
"Access to fetch at 'http://localhost:8080/api/log/batch/download?ids=752581017469976576&ids=752581021928521728' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.",
"Failed to load resource: net::ERR_FAILED",
"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",
@ -57,22 +61,22 @@
{
"step": "AOP 写端点采集素材(建/删演示角色)",
"ok": true,
"note": "写操作完成:建/删演示角色 e2d-日志演示角色-172728(等待采集落库后断言)"
"note": "写操作完成:建/删演示角色 e2d-日志演示角色-214637(等待采集落库后断言)"
},
{
"step": "操作日志分页(新增/删除行命中 + 时间范围逐字段)",
"ok": false,
"note": "失败: AOP 采集行未命中: 新增=false 删除=true"
"ok": true,
"note": "AOP 采集行命中:新增行 content=/api/roles/saveOrUpdate,操作人=罗伟健,结果=成功 逐字段:今日范围 10 行 / 2000-01-01 范围 0 行(时间过滤生效)"
},
{
"step": "登录日志「退出」行(内联埋点 + token 重铸)",
"ok": false,
"note": "失败: 登录日志「退出」行未命中(operatorName=罗伟健)"
"ok": true,
"note": "退出埋点命中:opType=退出,operatorName=罗伟健,status/result=成功,ip=0:0:0:0:0:0:0:1,opTime=2026-09-08 21:46:46 (to"
},
{
"step": "批量日志「导出」行(result+fileId+ZIP 魔数)",
"ok": true,
"note": "导出留档行命中:page=线索设置,result=成功,countDetail=成功导出 2 条公海池,fileId=752581016106827776 (导出字节 4204,ZIP 魔数 ✓)"
"note": "导出留档行命中:page=线索设置,result=成功,countDetail=成功导出 2 条公海池,fileId=752646072127455232 (导出字节 4205,ZIP 魔数 ✓)"
},
{
"step": "批量日志「导入」行(BatchResult 计数)",
@ -81,18 +85,18 @@
},
{
"step": "结果文件下载 /api/log/batch/file(ZIP 魔数)",
"ok": false,
"note": "失败: 结果文件非 ZIP(xlsx):102 字节"
"ok": true,
"note": "结果文件下载成功:日志行 id=752646072353947648(fileId=752646072127455232),4205 字节,ZIP 魔数 PK ✓(90 天内有效)"
},
{
"step": "打包下载 /api/log/batch/download ids[](ZIP 魔数)",
"ok": false,
"note": "失败: Failed to fetch"
"ok": true,
"note": "打包下载成功:ids=[752646072353947648, 752646076426616832],3660 字节,ZIP 魔数 PK ✓"
},
{
"step": "不存在文件 id 错误形态(40401/66001/404)",
"ok": false,
"note": "失败: 不存在的文件 id 竟返回 96 字节——未被拦截"
"ok": true,
"note": "不存在文件 id 错误形态生效:code=40401,批量操作日志不存在: 999999999999 (查无日志行→40401/HTTP404;过期→66001;行存在但无文件→66002;90 天分"
},
{
"step": "BUDDY 三 page 权限拒绝(ADMIN 门禁)",
@ -101,18 +105,18 @@
},
{
"step": "公海规则发布(部门专用)+ boundOppCount 基线",
"ok": false,
"note": "失败: 无可用发布中部门专用规则(含本部门)"
"ok": true,
"note": "规则就绪:id=752646085117214720,name=e2d-绑定数规则-214637,scope=2(部门专用),deptIds=[744841292348915712] boundOppCount 基线 base=16(本轮新建发布) 后续按增量"
},
{
"step": "商机 A → boundOppCount=base+1(match 语义)",
"ok": false,
"note": "失败: 先执行步骤1"
"ok": true,
"note": "商机 A(id=752646090414620672)→ boundOppCount=17(base+1 ✓,match 语义生效)"
},
{
"step": "商机 B → boundOppCount=base+2(动态统计非快照)",
"ok": false,
"note": "失败: 先执行步骤1"
"ok": true,
"note": "商机 B(id=752646094093025280)→ boundOppCount=18(base+2 ✓,动态统计非快照)"
},
{
"step": "默认规则兜底口径(无默认规则降级提示)",
@ -122,13 +126,18 @@
{
"step": "清理(商机 close 正门 + 规则停用)",
"ok": true,
"note": "清理完成:关停演示商机 0/0(close 终态;已关闭仍计数,仅 opp_status=5 排除——增量口径跨轮安全);无本轮规则"
"note": "清理完成:关停演示商机 2/2(close 终态;已关闭仍计数,仅 opp_status=5 排除——增量口径跨轮安全);演示规则已停用(发布行留存可辨)"
},
{
"step": "网络层零旧契约形态",
"ok": true,
"note": "bad_method=[] old_style=[] (共55请求)"
"note": "bad_method=[] old_style=[] (共47请求)"
},
{
"step": "console/page error 双零(预期 403 拒绝豁免)",
"ok": true,
"note": "raw_console=3 exempt_403=3 real=0 page_errors=0"
}
],
"all_pass": false
"all_pass": true
}

3
.scratch/a7-heavy/demo/index.html

@ -530,7 +530,8 @@ async function logPackDownload() {
body: ids.join('&') });
if (!r.ok) throw new Error('HTTP ' + r.status);
const buf = new Uint8Array(await r.arrayBuffer());
if (!isZip(buf)) { failBox('glogOut', new Error('打包下载非 ZIP: ' + buf.length + ' 字节')); return; }
// isZip 之外加字节数下限:空 ZIP(仅 end-of-central-dir)恰为 22 字节且魔数合法,文件全不可取时会误 PASS
if (!isZip(buf) || buf.length < 100) { failBox('glogOut', new Error('打包下载非 ZIP 或空包: ' + buf.length + ' 字节')); return; }
okBox('glogOut', '打包下载成功:ids=[' + ids.map(s => s.replace('ids=', '')).join(', ') + '],' + buf.length + ' 字节,ZIP 魔数 PK ✓');
} catch (e) { failBox('glogOut', e); }
}

10
.scratch/a7-heavy/demo_smoke.py

@ -10,14 +10,14 @@
任务行与登录/操作/批量日志行 = 审计数据留存不删同步设置由 demo 步骤7 自还原原值
商机 close 正门终态已关闭仍计数 增量断言 base+n 跨轮安全
"""
import json, re, time, pathlib, sys, io
import json, os, re, time, pathlib, sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
from playwright.sync_api import sync_playwright
ROOT = pathlib.Path(__file__).resolve().parent
SHOTS = ROOT / "shots"; SHOTS.mkdir(exist_ok=True)
DEMO = ROOT / "demo" / "index.html"
API = "http://localhost:8080"
SHOTS = pathlib.Path(os.environ.get("E2D_SHOTS") or (ROOT / "shots")); SHOTS.mkdir(exist_ok=True)
DEMO = pathlib.Path(os.environ.get("E2D_DEMO") or (ROOT / "demo" / "index.html"))
API = os.environ.get("E2D_API") or "http://localhost:8080"
ADMIN = "739564171091247104"
BUDDY = "744842318024015872"
TS = time.strftime("%H%M%S")
@ -270,7 +270,7 @@ summary = {"ts": TS, "console_errors": console_errors, "page_errors": page_error
"exempt_403": expected_403, "real_console_errors": real_console,
"results": results,
"all_pass": all(r["ok"] for r in results) and not real_console and not page_errors}
(ROOT / "demo-smoke-result.json").write_text(json.dumps(summary, ensure_ascii=False, indent=1), encoding="utf-8")
pathlib.Path(os.environ.get("E2D_RESULT") or (ROOT / "demo-smoke-result.json")).write_text(json.dumps(summary, ensure_ascii=False, indent=1), encoding="utf-8")
print("\n== SUMMARY ==")
for r in results:
print(("PASS " if r["ok"] else "FAIL ") + r["step"] + (" | " + r["note"] if r["note"] else ""))

32
.scratch/a7-heavy/issues/04-bruno-regen-baseline.md

@ -1,9 +1,12 @@
# 04 bruno 重生成 + 基线验收收官
Type: task
Status: open
Status: resolved
Blocked by: 01, 02, 03, 05
> Claimed 2026-09-08 收官会话,同日 resolved(见下 Answer)。
> 编排说明:e2e(票 05)前置于本收官票——一期经验(票 13 在收官票 12 之后发现 D-01 代码缺陷与 F-01 文档缺陷,修复穿透了收官验收);二期把 e2e 修复闭环后再做终态基线验收,收官验收覆盖修复后代码。
## Question
@ -14,3 +17,30 @@ Blocked by: 01, 02, 03, 05
2. **全仓基线验收**:*.java BOM 扫描 0 + mvn compile 过 + 全仓单测全绿(9 模块 + 新 crm-log = 10;注意跨模块集成测试内嵌 H2 DDL 是否需补新表列——一期票 12 有同款漏网先例)。
3. **记账清单终态标注**:a7-parity [gap-report.md](../../a7-parity/gap-report.md) §六「已记账」逐项标注执行结果(同步任务/全局日志/公海绑定数→已执行;数据共享 3 页→仍挂账等原型正式化);两 map 交叉链接闭合。
4. 未提交任何 git(后端仓 + 文档仓均留用户审查)。
## Answer
2026-09-08 收官执行完毕,验收全过:
### 1. bruno 对账双向零差(文档仓 E:\code\crm-api-docs,改动未提交 git)
- 对账通道 bruno_scan_d.py(customer-rework 期脚本复用)ROOTS 本票补 `crm-log/src/main/java`——票 02 只改了 bruno-sync.config.json,对账脚本滞后,不补则 crm-log 5 端点误报 delete。
- 首跑 381=381 但 1 new / 1 delete:「同步任务详情.bru」docs key 写成 `GET /api/system/sync/tasks/detail?id=`(票 01 落文件笔误,全仓 381 份孤例,其余 detail 类 key 均干净);修 key 去 `?id=` 后复跑:**source_key_count 381 = generated_file_count 381,keep 381 / new 0 / delete 0 / unrecognized 0 / conflicts 0 / private 0——双向零差**。数字闭环:381 = 一期终态 372 + 同步任务 5(票 01)+ 日志 5(票 02)− 旧 POST /sync/dingtalk key(任务化后消失)。
- 本票无新增 .bru:票 01/02 的 10 份 + 票 03 的 1 处 patch 已各自落位,收官对账即验收。
### 2. 基线验收
- 全仓 *.java BOM 扫描 0(.scratch/bom-scan.ps1)。
- mvn test 全链路(test 阶段蕴含 compile)BUILD SUCCESS 1:22 min:12 reactor 全 SUCCESS,10 测试模块 **1290 用例 0 失败**(base 44 / file 84 / auth 241 / dict 63 / rule 218 / preference 23 / customer 131 / lead 186 / opportunity 266 / log 34)。
- 票面警示的「跨模块集成测试内嵌 H2 DDL 漏网」(一期票 12 同款)未复现:crm-log 三表与 dt_sync_task 无跨模块测试引用。
- 环境事实:跑测前停掉票 05 遗留起服(PID 7764)释放内存,防 C2 原生 OOM 重演;MAVEN_OPTS 限堆 + TieredStopAtLevel=1 通道沿用票 05。
### 3. 记账清单终态标注与交叉链接
- gap-report §六追加二期收官标注:原「已记账」三项(同步任务 / 全局日志 / 公海绑定数)转**已执行**,数据共享 3 页仍挂账等原型正式化。
- 两 map 交叉链接闭合:a7-parity map 尾注更新(执行票 01–05 + 二期收官段);a7-heavy map Decisions so far 追加本票行并宣告本 effort 收官。
### 4. git 状态
- 后端仓:本票零源码改动,仅 .scratch(对账脚本 ROOTS 一行 + 本票脚手架);全部未提交留用户审查。
- 文档仓:「同步任务详情.bru」docs key 修正 1 处;未提交留用户审查。

28
.scratch/a7-heavy/issues/05-e2e-demo-smoke.md

@ -1,9 +1,12 @@
# 05 a7-heavy 补码成果 Demo E2E(浏览器级冒烟)
Type: task
Status: claimed
Status: resolved
Blocked by: 01, 02, 03
> Claimed 2026-09-08 wayfinder session(handoff-a7-heavy-05 换机续接收尾),同日 resolved(见下 Answer)。
## Question
参照一期 [13-e2e-demo-smoke](../../a7-parity/issues/13-e2e-demo-smoke.md) 模式,对票 01–03 落地的二期补码成果做浏览器级 e2e 验证。底座**复用 a7-parity demo**(CSS / uid 直登 / toast / 弹窗 / api helper / 步骤面板 JS 模式,沿用 v2 修订后形态——`outBox/logLine` 助手收裸 id 走 `document.getElementById`),**新建** `.scratch/a7-heavy/demo/index.html`(不改动一期 demo),tab 仅覆盖二期三域;TS 经 URL `?ts=` 注入对齐脚本侧。
@ -31,3 +34,26 @@ Blocked by: 01, 02, 03
运行环境:同一期(verify profile + MinIO env 起服 fat jar,完整命令见一期 `demo_smoke.py` 头注)。
验收基线:全 PASS 两轮幂等重跑 + console/page error 双零 + 网络层零旧契约形态(零 PUT/DELETE、零路径变量)。
## Answer
2026-09-08 执行落地。22 步浏览器级冒烟最终**两轮 ALL_PASS**,验收基线全部达成(全 PASS 两轮幂等 + console/page error 双零 + 网络层零旧契约形态:47 请求零 PUT/DELETE、零路径变量)。
**交付物**:`.scratch/a7-heavy/demo/index.html`(3 tab:同步任务/全局日志/公海绑定数,复用一期 demo 底座)+ `demo_smoke.py`(22 步 + pre_clean 幂等 + 截图 shots/ + 结果落 demo-smoke-result.json)。
**轮次轨迹**:首轮 13 PASS / 9 FAIL → 探针定位 5 根因全修 → 21 PASS / 1 FAIL(暴露环境根因,见下)→ 注入 MinIO 凭据 + demo 断言收紧 → 22/22 两轮 ALL_PASS。
**缺陷修复 5 根因(探针轮)**:
1. 源码:`OpLogAspect.resolveOpType` 补 `saveorupdate` 特判(必须先于 update 子串判定),`/api/roles/saveOrUpdate` 由误采「修改」纠正为「新增」。
2. demo:登录日志行断言字段 result → status;`batch/file?id=` 收日志行主键(requireBatchLog→row.fileId,非 fileId 本身);`batch/download` 改 POST(GET 405 且无 CORS 头);`opp-pool-rule/publish` 四旗标全必传(补 recycleRemindEnabled=0,缺则 64008)。
3. smoke:BUDDY 403 的浏览器 console error("status of 403")按预期拒绝豁免,豁免后仍要求真零。
**收尾会话(handoff-a7-heavy-05 换机续接)两段新事实**:
1. **fat jar 重打**:旧 `.jar.original`(9/6 构建)挡 repackage rename → 删除后重打;首次构建遇 JVM C2 原生 OOM(`Chunk::new`,机器 15.4G 物理内存当时仅余 0.4G)→ `MAVEN_OPTS=-Xmx512m -XX:TieredStopAtLevel=1` 关 C2 限堆重试,BUILD SUCCESS 56s,jar 时间戳确认更新(规避增量静默跳过坑)。
2. **第 6 步复跑仍 FAIL(91 字节非 ZIP)→ 根因在环境非代码**:本机起服从未注入 MinIO 凭据(application.yml 兜底 minioadmin ≠ 远程真凭据,启动横幅无报错、直到文件类操作才炸),留档结果文件上传/下载全挂——连带发现第 7 步「打包下载 22 字节 PASS」实为**空 ZIP 误报**(仅 end-of-central-dir 恰 22 字节且魔数合法)。对策:起服注入 `CRM_MINIO_AK` / `CRM_MINIO_SK`(凭据用户实锤,见 customer-e2e 换机环境事实 memory,不落任何仓库文件);demo 第 7 步断言收紧为 `isZip + ≥100 字节`。修复后导出留档真实落库(fileId 对应 4205 字节 ZIP)、单文件下载 4205 字节、打包下载 3661 字节;两轮 ALL_PASS(boundOppCount base 12→14→16,增量口径跨轮安全;pre_clean 每轮清上轮 e2d-导入池;SYNC-0908-01 运行中任务复用走 61023 态,两态容忍设计如约生效)。
**清理**:`probe_once.py` 已删;`/api/roles/page?roleName=e2d-probe` 无残留(角色表仅内置「管理员」)。
**换机后本机环境事实(沉淀)**:构建通道 = IDEA bundled maven + JBR(PATH 无 mvn/java);smoke 解释器 = `.scratch/customer-e2e/.venv10` 的 python -X utf8(playwright + openpyxl,chromium-1234 走默认缓存 `%LOCALAPPDATA%\ms-playwright`);**本机起服完整前提 = verify profile + MinIO AK/SK 环境变量**——此为本票新增硬前提,缺则票 02 全部文件类断言(file/download/ZIP 魔数)必挂且失败形态是 91 字节 JSON 错误信封,极易误判为 demo 缺陷。

3
.scratch/a7-heavy/map.md

@ -25,6 +25,9 @@ Label: wayfinder:map · charted 2026-09-08
- 2026-09-08 票 01 执行落地(resolved):同步任务 5 端点任务化全量实现——契约改型返 `DtSyncLaunchDTO{taskId, taskNo}`、launch 并发占位 61023、单线程池异步、进度 5/45/90/100;**组织拉取抽离 `OrgSourcePort` SPI(执行中用户拍板)**:`DingTalkOrgSource` 实现 + 装配缝 `crm.auth.org-source`(缺省 dingtalk),编排层 `DtOrgSyncServiceImpl` 平台无关化零 SDK 依赖,后续企微/自有 OA 新增实现类即可;lastSyncTime 直接切换 dt_sync_task 最新成功行(Redis 兼容读取未做);重试规则仅落设置+边界校验不实现自动重试;partial_failed 枚举保留但不产生;错误码 61022-61024、权限种子 @Order(25)、bruno 5 份落文档仓;验证 crm-auth 全量单测 EXIT=0 + BOM 0。
- 2026-09-08 票 02 执行落地(resolved):**全局日志 crm-log 新模块**(轻模块不并入 crm-auth),三表 sys_log_operation/login/batch;**采集机制拍板①混合**:AOP 注解切面(全仓首个 @Aspect,补 spring-boot-starter-aop)兑底全 Controller POST + crm-base `OpLogPort` SPI 内联埋点(crm-auth 登录/退出 5 钩子 + crm-rule LeadPool 批量 3 处,ObjectProvider 容忍缺 Bean,依赖方向零循环);**覆盖面拍板②写侧先行**:不采「查询」GET(量化 81 GET/188 POST),op_type 8 枚举保留定义;批量结果文件 collector 统一落盘 + 90 天保留(下载校验 + cleanup task 3:30 错峰);权限种子 @Order(26);**错误码取 66xxx 段(非票面 65xxx——65001~65999 已被 crm-lead 占用,README 已登记)**;bruno 文档 5 份落文档仓;验证编译过 + BOM 0 + crm-log 34 用例 + 全仓回归 1273 用例全绿。
- 2026-09-08 票 [03](issues/03-pool-rule-bound-count.md) 执行落地(resolved):方案 B detail 动态统计零 DDL;**端口落位偏差**——计数 port 归 `com.crm.rule.port.outbound.PoolRuleOpportunityCountPort`(票面 crm-base 偏差,依 ADR-0025 同消费方同方向先例,实现归 crm-opportunity adapter,零 pom 改动,[ADR-0035](../docs/adr/0035-pool-rule-bound-count-port-seam.md));**page 不补列**(票面「原型列头在列表页」前提被 v29 推翻,仅 detail 回填);口径三定:公海态照常计入(owner_dept_id 保留即 match 输入)/ 排除已转项目 opp_status=5 / port 缺失 fail-open 按 0 展示;默认规则 = 不命中发布中专用规则的商机数(排除集 = 发布中专用规则部门并集);detail 出参 `boundOppCount`(Long→string 序列化);bruno 仅 patch 公海规则详情.bru 结构表一行(实测示例 JSON 不动;分页不补——BaseDTO NON_NULL 下 page 无此键,结构表补行反而撒谎);验证 mvn EXIT=0(crm-rule 32 + crm-opportunity 4 用例全绿)+ BOM 0。
- 2026-09-08 票 [05](issues/05-e2e-demo-smoke.md) 执行落地(resolved):demo/index.html(3 tab 二期三域,复用一期底座)+ demo_smoke.py 22 步**两轮 ALL_PASS**;探针修 5 根因(OpLogAspect saveorupdate 特判先于 update 判定 + demo 4 处 + smoke BUDDY 403 豁免);**新增硬环境事实:本机起服必须注入 CRM_MINIO_AK/SK**——application.yml 兜底 minioadmin 连不通远程 MinIO,留档上传/下载全挂,且打包下载会以 22 字节空 ZIP 误 PASS(demo 断言已收紧 isZip + ≥100B);fat jar 重打遇 C2 原生 OOM(内存压力),MAVEN_OPTS 限堆 + TieredStopAtLevel=1 解;probe_once.py 已删、e2d-probe 角色无残留。
- 2026-09-08 票 [04](issues/04-bruno-regen-baseline.md) 执行落地(resolved):**本 effort 收官**。bruno 对账双向零差——bruno_scan_d.py ROOTS 本票补 crm-log(对账脚本滞后于 bruno-sync.config.json);修同步任务详情.bru docs key 多 `?id=` 后缀孤例笔误后 source 381 = generated 381(keep/new/delete/unrecognized/conflicts 全 0;381 = 一期终态 372 + 同步任务 5(票 01)+ 日志 5(票 02)− 旧 POST /sync/dingtalk key 任务化消失)。基线验收:BOM 0 + mvn test 全链路 BUILD SUCCESS(12 reactor 全 SUCCESS,10 测试模块 1290 用例 0 失败:base 44 / file 84 / auth 241 / dict 63 / rule 218 / preference 23 / customer 131 / lead 186 / opportunity 266 / log 34;跑前停服防 C2 原生 OOM 重演,MAVEN_OPTS 限堆通道沿用);票面警示的跨模块 H2 DDL 漏网未复现。记账标注:gap-report §六 三项转已执行 + 两 map 交叉链接闭合。执行票 01–05 全 resolved;后端仓(仅 .scratch)与文档仓(1 处 key 修正)均未提交 git,留用户审查。
## Not yet specified

84
.scratch/a7-heavy/probe_once.py

@ -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"))

BIN
.scratch/a7-heavy/shots/03-sync-stats.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

BIN
.scratch/a7-heavy/shots/04-glog-prepare.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 77 KiB

BIN
.scratch/a7-heavy/shots/05-glog-export.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

BIN
.scratch/a7-heavy/shots/06-glog-denied.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 76 KiB

BIN
.scratch/a7-heavy/shots/07-poolrule-base.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 76 KiB

BIN
.scratch/a7-heavy/shots/08-cleanup.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 72 KiB

10
.scratch/a7-parity/demo_smoke.py

@ -10,14 +10,14 @@
附带断言: 网络层零 PUT/DELETE /api/<module>/<digits>/ 路径变量console/page error
清理: 演示角色/权限点/规则草稿/公海池 API 正门删除BUDDY 角色还原移交一来一回净零
"""
import json, re, time, pathlib, sys, io
import json, os, re, time, pathlib, sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
from playwright.sync_api import sync_playwright
ROOT = pathlib.Path(__file__).resolve().parent
SHOTS = ROOT / "shots"; SHOTS.mkdir(exist_ok=True)
DEMO = ROOT / "demo" / "index.html"
API = "http://localhost:8080"
SHOTS = pathlib.Path(os.environ.get("E2D_SHOTS") or (ROOT / "shots")); SHOTS.mkdir(exist_ok=True)
DEMO = pathlib.Path(os.environ.get("E2D_DEMO") or (ROOT / "demo" / "index.html"))
API = os.environ.get("E2D_API") or "http://localhost:8080"
ADMIN = "739564171091247104"
BUDDY = "744842318024015872"
TS = time.strftime("%H%M%S")
@ -474,7 +474,7 @@ xlsx.unlink(missing_ok=True)
summary = {"ts": TS, "console_errors": console_errors, "page_errors": page_errors,
"results": results,
"all_pass": all(r["ok"] for r in results) and not console_errors and not page_errors}
(ROOT / "demo-smoke-result.json").write_text(json.dumps(summary, ensure_ascii=False, indent=1), encoding="utf-8")
pathlib.Path(os.environ.get("E2D_RESULT") or (ROOT / "demo-smoke-result.json")).write_text(json.dumps(summary, ensure_ascii=False, indent=1), encoding="utf-8")
print("\n== SUMMARY ==")
for r in results:
print(("PASS " if r["ok"] else "FAIL ") + r["step"] + (" | " + r["note"] if r["note"] else ""))

1
.scratch/a7-parity/gap-report.md

@ -66,3 +66,4 @@
- **已执行**:缺接口 9 组中 7 组落地(用户管理 4 / 角色 4 / 权限点 3 / 推送规则 8 / 查重规则 8 / 项目规则 6 / LeadPool 1 字段+2 端点);缺入出参全落(UserPageParam×4 / UserStats 时间戳×3 / RoleParam×2 / RoleDTO×3 / 阶段模板条件式档);语义差异按票 06 拍板兑现(资源删除级联→禁删等);tag 12 处全修;bruno-sync 重生成对账双向零差(372=372)。
- **已记账(本期不做)**:同步任务 5 端点(钉钉任务调度对接);全局日志 5 端点+新全局表+AOP+批量结果文件 90 天管理(归属新建 crm-log);公海规则 detail「绑定商机数量」出参(P2,依赖商机侧引用字段);A7-2-3 数据共享 3 页(等原型正式化)。
- **2026-09-08 更新**:前三项已毕业为二期 effort [a7-heavy](../a7-heavy/map.md)(建票 01 同步任务 / 02 全局日志 crm-log / 03 公海绑定数 / 04 收官对账)。
- **2026-09-08 二期收官标注**:[a7-heavy](../a7-heavy/map.md) 执行票 01–05 全 resolved(收官票 [04](../a7-heavy/issues/04-bruno-regen-baseline.md)),原「已记账」三项全部转**已执行**——同步任务 5 端点(任务化 + 定时调度 + OrgSourcePort SPI,票 01)、全局日志 5 端点 + crm-log 新模块 + AOP 兜底 + OpLogPort 埋点 + 批量结果文件 90 天管理(票 02)、公海规则 detail `boundOppCount` 动态统计(票 03,PoolRuleOpportunityCountPort,ADR-0035);e2e 票 05 两轮 22/22 ALL_PASS 前置于收官。终态基线:bruno 对账双向零差(381=381)、全仓 10 模块 1290 测试 0 失败、BOM 0。**数据共享 3 页仍挂账等原型正式化**(唯一遗留记账项)。

4
.scratch/a7-parity/map.md

@ -53,4 +53,6 @@ A7 后台管理全分区(正式树 31 页:A7-1 用户管理 / A7-2 权限管
- swagger UI 美化、sys_menu 种子菜单数据迁移(ADR-0024 既有遗留)。
- 原型中非 A7 空缺模块(A1X 工作计划、A6 数据看板等)的接口实现。
> **2026-09-08 二期建票**:票 12 记账清单中「同步任务 5 端点 / 全局日志 5 端点 + AOP + 批量结果文件 90 天管理 / 公海规则绑定商机数量」三项毕业为二期 effort [a7-heavy](../a7-heavy/map.md)(执行票 01–04);「数据共享 3 页」仍挂账等原型正式化。
> **2026-09-08 二期建票**:票 12 记账清单中「同步任务 5 端点 / 全局日志 5 端点 + AOP + 批量结果文件 90 天管理 / 公海规则绑定商机数量」三项毕业为二期 effort [a7-heavy](../a7-heavy/map.md)(执行票 01–05,e2e 票 05 后补毕业并前置于收官);「数据共享 3 页」仍挂账等原型正式化。
>
> **2026-09-08 二期收官**:a7-heavy 执行票 01–05 全 resolved——bruno 对账双向零差(381=381)、全仓 10 模块 1290 测试 0 失败、BOM 0;缺口终态标注见 [gap-report §六](gap-report.md)。

13
.scratch/customer-contact-graph/issues/00-prototype-demo.md

@ -29,3 +29,16 @@
- UI.md 3-变体默认不适用:设计权威=Lanhu 原型(原型第一),唯一自由度(SVG vs DOM 渲染、自动布局规则)做成页内可见可挑战的规则面板。
- 原型自身不一致处(列表「共 5 位」vs 图谱 9 卡片、列表江小白=人事经理 vs 图谱=投标商务·其他)在 demo 页脚记录,以图谱侧为权威。
## 实现与自检记录(2026-09-09 续接会话)
- **已落盘**:`prototype/prototype-contact-graph.html` 单文件 1334 行(head+CSS 185 + body/JS 1149),零后端依赖;body/JS 按 handoff §3 定稿重建(AGENTS.md 降级链:分块写 → python 合并 → 回读验证)。
- **实现范围**:图谱查看态(tier 布局/公司节点/未识别 dashed 区/顶层徽标/悬停气泡/拖画布+缩放+适应)、编辑态全套(绘制橙钮/提示条/圆点/拖卡/底点拖出连线/重复「已存在连线」/auto 删边二次确认/保存 DFS 环检测红色高亮/重置确认/完成绘制三选/切视图拦截)、脱敏拍板口径(列表+图谱卡片默认脱敏→点击明文+埋点→显示完整号码批量揭→导出 CSV 恒脱敏)、6.2 快速添加(类别→职务二级联动/空行不提交/查重+确认知悉/首错行定位)、6.3 页内编辑(行内编辑/dirty 高亮/撤销修改/保存全部校验/职务变更触发自动边重建)、右侧规则/状态/日志三 tab 面板(自动边规则/脱敏分歧黄框/vetoed 对/埋点日志)。
- **自检**:Playwright(`.venv10`,file:// 直开)**25/25 PASS,0 console/page error**;截图 7 张在 `prototype/shots/`;结果 JSON:`prototype/selfcheck-result.json`;重跑入口:`prototype/selfcheck.py`。
- **自检期修复 3 处**:① init 缺 `layoutGraph()`(卡片叠原点);② edge-hit path 缺 `fill:none`(贝塞尔闭合区被填成黑块);③ 6.2 入口纠正 = 原型 u193「添加联系人」按钮(OCR快捷新增保持占位,与原型一致)。
- **待验收**:图谱 u536 逐条对照 + 脱敏分歧裁决(规则面板黄框)→ 通过后关票、升格正式 demo(票 06 接真实 API)并重启票 01 grilling。
- **验收反馈修订 ①(2026-09-09 用户)**:未设层级联系人作为连线端点时先弹层输入层级(默认 = 对端层级 ±1)→ 定级后移出「未识别层级」区、自动边按新层级重建;重置/放弃可回退层级(快照含 levels)。已实现 + 自检 29/29 PASS(`shots/05b-level.png`);衍生票 01 待钉点:职务字典空层级被图谱定级覆盖后的持久化语义(回写职务 vs 图谱侧独立存层级)。
- **验收反馈修订 ②(2026-09-09 用户「连不了线」)**:根因 = 三坑叠加——①蓝圆点命中区仅 12×12 且半悬卡外(点偏即变拖卡)→ CSS ::after 扩到 30×30;②顶点拖出无反应(只认底点出发)→ 支持顶点反向拖(dir=up,目标=上级);③初始 12 条 auto 边已覆盖最自然组合,重复拒绝 toast 只有四个字 → 带上具体人名(「已存在连线:王强 → 胡子(蓝色实线即是)」)。自检升级为真实鼠标全链路(`pg.mouse` mousedown→move→up),32/32 PASS。
- **验收反馈修订 ③(2026-09-09 用户)**:层级值域改为 **0–100、100 = 顶层**(用户拍板)→ 字典/数据 level 数值化(100/60/30 为示例);布局改「只有 100 在公司节点上方,0–99 按层级降序分行,未识别 null 进 dashed 区」;图谱卡片右上角加层级徽标(lv-chip);连线定级弹窗改 0–100 数字输入(默认 = 对端层级 ∓10);自动边规则重述为「相邻更高一档,同类别优先」(语义与旧三档版等价)。修复:layoutGraph 改按档强制归行(档位插入使旧位置不可信)。自检 33/33 PASS,`shots/05b-level.png` 确认。实现注:SearchReplace 大 CJK 块再次触发部分落盘(首次调用仅 contacts 一条失败其余落盘),长块改造按 AGENTS.md 降级链改 python 特征行定位切片替换完成。
- **验收反馈修订 ④(2026-09-09 用户)**:①**一个节点最多只有一个上级**(单父约束,手动/自动同样生效:目标已有上级 → 拒绝连线并提示先删旧线);②**层级 100 无上级、下级就是公司** → 自动连「100 → 公司」系统边(可删否决;公司节点不受单父约束,允许多个 100 并列挂上);③自动边改单父化:相邻更高一档(同类别优先)**恰好一人**才自动连,多候选属歧义不自动连留手动(**有层级节点可以没上级**,用户同日补充确认;待票 01 裁决:多候选是否改为择一自动连)。初始 auto 边 12 → 4(3×100→公司 + 邓伦→胡子)。环测试换无上级对(张华↔王大牛)。自检 34/34 PASS(`shots/03-graph.png` 确认),实现仍走 python 特征行定位切片替换(SearchReplace 大 CJK 降级链)。
- **验收反馈修订 ⑤(2026-09-09 用户)**:连线**无向化**——①引脚只管连线,不再区分底点/顶点方向语义,拖任一引脚到对方卡片即可;②**上下级由两端层级自动决定**(层级高者为上级),与拖动引脚无关;③**同级(层级相同)之间不连线**,即时拒绝;④连带推论:连线层级严格递减 → 图恒为有向无环,环在构造上不可能(DFS 检测降为保存时兕底),自检环场景改为「反向同对必命中已存在」;⑤定级弹窗默认值简化为对端层级 −10(定级后方向自动定)。实现:tryConnect 改无向签名 (aId,bId) + 新增 commitDirectionalEdge(同级拒→定方向→单父拒→落 pending);openLevelPicker 确定回调改走 commitDirectionalEdge。自检 34/34 PASS。**⑤补丁(用户当日反馈「同级了连线没清掉」)**:建线后层级再变化(定级/职务变更)会让既有线变同级或方向颠倒 → 新增 sanitizeManualEdges(rebuildAutoEdges 末尾调用):manual committed 边与 pending 新增中,父层级 ≤ 子层级的自动移除并记日志(auto 边由重建构造保证合法)。自检 35/35 PASS。

1243
.scratch/customer-contact-graph/prototype/prototype-contact-graph.html

File diff suppressed because it is too large

180
.scratch/customer-contact-graph/prototype/selfcheck-result.json

@ -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": []
}

235
.scratch/customer-contact-graph/prototype/selfcheck.py

@ -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)

BIN
.scratch/customer-contact-graph/prototype/shots/01-list.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

BIN
.scratch/customer-contact-graph/prototype/shots/02-reveal.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

BIN
.scratch/customer-contact-graph/prototype/shots/03-graph.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

BIN
.scratch/customer-contact-graph/prototype/shots/05-cycle.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

BIN
.scratch/customer-contact-graph/prototype/shots/05b-level.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

BIN
.scratch/customer-contact-graph/prototype/shots/06-qa-added.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

BIN
.scratch/customer-contact-graph/prototype/shots/07-jobchange-graph.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

BIN
.scratch/customer-contact-graph/prototype/shots/08-veto.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

BIN
.scratch/customer-contact-graph/prototype/shots/09-realdrag.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

176
.scratch/customer-rework/bruno-recon-d.json

@ -1,12 +1,12 @@
{
"report": {
"source_api_count": 265,
"source_key_count": 372,
"generated_file_count": 372,
"source_api_count": 274,
"source_key_count": 381,
"generated_file_count": 381,
"private_file_count": 0,
"unrecognized_count": 0,
"key_conflicts": [],
"keep_count": 372,
"keep_count": 381,
"new_count": 0,
"delete_file_count": 0,
"delete_distinct_url_count": 0
@ -1910,6 +1910,36 @@
"generated": true,
"folder": "项目规则"
},
{
"path": "A7 后台管理/日志管理/批量操作日志/批量下载结果文件.bru",
"key": "POST /api/log/batch/download",
"generated": true,
"folder": "批量操作日志"
},
{
"path": "A7 后台管理/日志管理/批量操作日志/批量操作日志分页.bru",
"key": "POST /api/log/batch/page",
"generated": true,
"folder": "批量操作日志"
},
{
"path": "A7 后台管理/日志管理/批量操作日志/结果文件下载.bru",
"key": "GET /api/log/batch/file",
"generated": true,
"folder": "批量操作日志"
},
{
"path": "A7 后台管理/日志管理/用户操作日志/用户操作日志分页.bru",
"key": "POST /api/log/operation/page",
"generated": true,
"folder": "用户操作日志"
},
{
"path": "A7 后台管理/日志管理/登录日志/登录日志分页.bru",
"key": "POST /api/log/login/page",
"generated": true,
"folder": "登录日志"
},
{
"path": "A7 后台管理/权限管理/权限点管理/上传图标.bru",
"key": "POST /api/resources/icon/upload",
@ -2060,6 +2090,30 @@
"generated": true,
"folder": "用户管理"
},
{
"path": "A7 后台管理/用户管理/同步任务/保存同步设置.bru",
"key": "POST /api/system/sync/settings",
"generated": true,
"folder": "同步任务"
},
{
"path": "A7 后台管理/用户管理/同步任务/同步任务分页.bru",
"key": "POST /api/system/sync/tasks/page",
"generated": true,
"folder": "同步任务"
},
{
"path": "A7 后台管理/用户管理/同步任务/同步任务详情.bru",
"key": "GET /api/system/sync/tasks/detail",
"generated": true,
"folder": "同步任务"
},
{
"path": "A7 后台管理/用户管理/同步任务/读取同步设置.bru",
"key": "GET /api/system/sync/settings",
"generated": true,
"folder": "同步任务"
},
{
"path": "A7 后台管理/用户管理/同步任务/钉钉组织同步.bru",
"key": "POST /api/system/sync/dingtalk",
@ -2468,6 +2522,56 @@
],
"summary": "停用角色(内置角色不可停用)"
},
{
"file": "crm-auth/src/main/java/com/crm/auth/controller/SyncTaskController.java",
"className": "SyncTaskController",
"http": "POST",
"url": "/api/system/sync/dingtalk",
"tags": [
"A7 后台管理/用户管理/同步任务"
],
"summary": "发起钉钉组织同步任务(异步执行,返回任务编号)"
},
{
"file": "crm-auth/src/main/java/com/crm/auth/controller/SyncTaskController.java",
"className": "SyncTaskController",
"http": "POST",
"url": "/api/system/sync/tasks/page",
"tags": [
"A7 后台管理/用户管理/同步任务"
],
"summary": "同步记录分页(任务编号/开始时间范围/触发方式/执行状态筛选,开始时间倒序)"
},
{
"file": "crm-auth/src/main/java/com/crm/auth/controller/SyncTaskController.java",
"className": "SyncTaskController",
"http": "GET",
"url": "/api/system/sync/tasks/detail",
"tags": [
"A7 后台管理/用户管理/同步任务"
],
"summary": "同步任务详情(执行结果/失败明细/进度)"
},
{
"file": "crm-auth/src/main/java/com/crm/auth/controller/SyncTaskController.java",
"className": "SyncTaskController",
"http": "GET",
"url": "/api/system/sync/settings",
"tags": [
"A7 后台管理/用户管理/同步任务"
],
"summary": "读同步设置(含派生的当前计划/下次执行时间/最近修改人)"
},
{
"file": "crm-auth/src/main/java/com/crm/auth/controller/SyncTaskController.java",
"className": "SyncTaskController",
"http": "POST",
"url": "/api/system/sync/settings",
"tags": [
"A7 后台管理/用户管理/同步任务"
],
"summary": "保存同步设置(校验:HH:mm 格式、重试间隔≥1 分钟、重试次数≥1、并发≥1)"
},
{
"file": "crm-auth/src/main/java/com/crm/auth/controller/SystemController.java",
"className": "SystemController",
@ -2588,16 +2692,6 @@
],
"summary": "导出用户列表(按筛选条件,返回文件信息)"
},
{
"file": "crm-auth/src/main/java/com/crm/auth/controller/SystemController.java",
"className": "SystemController",
"http": "POST",
"url": "/api/system/sync/dingtalk",
"tags": [
"A7 后台管理/用户管理/同步任务"
],
"summary": "钉钉组织全量同步(部门树+用户,幂等可重跑)"
},
{
"file": "crm-auth/src/main/java/com/crm/auth/controller/SystemController.java",
"className": "SystemController",
@ -4285,7 +4379,7 @@
"A7 后台管理/系统设置/数据字典/字典项",
"A7 后台管理/业务规则/商机规则/商机相关字典"
],
"summary": "字典项分页(groupId/keyword/status 筛选,附带默认/引用/有效可选标记)"
"summary": "字典项分页(只查一级,二级子项随行内嵌 children 含停用返回;groupId/keyword/status 筛选只作用一级行,附带默认/引用/有效可选标记)"
},
{
"file": "crm-dict/src/main/java/com/crm/dict/controller/DictItemController.java",
@ -4296,7 +4390,7 @@
"A7 后台管理/系统设置/数据字典/字典项",
"A7 后台管理/业务规则/商机规则/商机相关字典"
],
"summary": "新建/编辑字典项(键值必填;编码空自动生成、创建后只读;未被引用且无子项可换组;默认项禁停)"
"summary": "新建/编辑字典项(children 非空即复合保存:子项全量替换——带 id 更新/不带 id 新建且键值=名称/缺失删除且被引用63009内置63003拦;复合新增不传键值、服务端取字典名称;一级或子项可带 isDefault,同载荷最多一行;键值必填63001;编码空自动生成、创建后只读;未被引用且无子项可换组;默认项禁停)"
},
{
"file": "crm-dict/src/main/java/com/crm/dict/controller/DictItemController.java",
@ -5002,6 +5096,56 @@
"A4 客户管理/我的客户/看板列表"
],
"summary": "看板单列卡片"
},
{
"file": "crm-log/src/main/java/com/crm/log/controller/LogController.java",
"className": "LogController",
"http": "POST",
"url": "/api/log/operation/page",
"tags": [
"A7 后台管理/日志管理/用户操作日志"
],
"summary": "用户操作日志分页"
},
{
"file": "crm-log/src/main/java/com/crm/log/controller/LogController.java",
"className": "LogController",
"http": "POST",
"url": "/api/log/login/page",
"tags": [
"A7 后台管理/日志管理/登录日志"
],
"summary": "登录日志分页"
},
{
"file": "crm-log/src/main/java/com/crm/log/controller/LogController.java",
"className": "LogController",
"http": "POST",
"url": "/api/log/batch/page",
"tags": [
"A7 后台管理/日志管理/批量操作日志"
],
"summary": "批量操作日志分页"
},
{
"file": "crm-log/src/main/java/com/crm/log/controller/LogController.java",
"className": "LogController",
"http": "POST",
"url": "/api/log/batch/download",
"tags": [
"A7 后台管理/日志管理/批量操作日志"
],
"summary": "批量下载勾选记录的结果文件(zip 打包)"
},
{
"file": "crm-log/src/main/java/com/crm/log/controller/LogController.java",
"className": "LogController",
"http": "GET",
"url": "/api/log/batch/file",
"tags": [
"A7 后台管理/日志管理/批量操作日志"
],
"summary": "单条批量日志结果文件下载(90 天保留期,过期报文件已过期)"
}
]
}

2
.scratch/customer-rework/bruno_scan_d.py

@ -14,7 +14,7 @@ ROOTS = [
"crm-auth/src/main/java", "crm-file/src/main/java", "crm-lead/src/main/java",
"crm-opportunity/src/main/java", "crm-rule/src/main/java",
"crm-preference/src/main/java", "crm-dict/src/main/java",
"crm-customer/src/main/java",
"crm-customer/src/main/java", "crm-log/src/main/java",
]
PROJECT = r"e:\code\crm-backend-matt"
DOCS = r"E:\code\crm-api-docs"

55
logs/a7-acc-heavy.log

@ -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

121
logs/a7-acc-parity.log

@ -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

121
logs/a7-acc-parity2.log

@ -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

1431
logs/crm-app.log

File diff suppressed because it is too large

1643
logs/mvn-pkg-a7h05.log

File diff suppressed because it is too large

3732
logs/mvn-test-a7h04.log

File diff suppressed because it is too large
Loading…
Cancel
Save