From 43887d31ac496d48b313b33c3dd6fef0193bd526 Mon Sep 17 00:00:00 2001 From: luoweijian <1329394916@qq.com> Date: Thu, 3 Sep 2026 18:59:30 +0800 Subject: [PATCH] =?UTF-8?q?docs(customer):=20=E8=B7=A8=E6=9C=BA=E5=99=A8?= =?UTF-8?q?=E4=BA=A4=E6=8E=A5=E5=8C=85=EF=BC=88=E4=BA=A4=E6=8E=A5=E6=96=87?= =?UTF-8?q?=E6=A1=A3=20+=20=E5=86=92=E7=83=9F/=E5=9B=9E=E5=BD=92=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E8=84=B1=E6=95=8F=E5=85=A5=E5=BA=93=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tools/ 三脚本凭据改环境变量(CRM_TOKEN/CRM_DB_*),无明文密码/内网IP/本机绝对路径;HANDOFF-CROSS-MACHINE-2026-09-03.md 为换机器权威交接(含新机器 bootstrap 步骤与期望基线 521/0/0 + BOM 零命中) --- .../customer-module/HANDOFF-2026-09-03.md | 1 + .../HANDOFF-CROSS-MACHINE-2026-09-03.md | 62 ++++++++++ .../customer-module/tools/regress_check.py | 30 +++++ .scratch/customer-module/tools/smoke-t12m.py | 117 ++++++++++++++++++ .scratch/customer-module/tools/smoke-t12m2.py | 58 +++++++++ 5 files changed, 268 insertions(+) create mode 100644 .scratch/customer-module/HANDOFF-CROSS-MACHINE-2026-09-03.md create mode 100644 .scratch/customer-module/tools/regress_check.py create mode 100644 .scratch/customer-module/tools/smoke-t12m.py create mode 100644 .scratch/customer-module/tools/smoke-t12m2.py diff --git a/.scratch/customer-module/HANDOFF-2026-09-03.md b/.scratch/customer-module/HANDOFF-2026-09-03.md index e8881e6..adf9fad 100644 --- a/.scratch/customer-module/HANDOFF-2026-09-03.md +++ b/.scratch/customer-module/HANDOFF-2026-09-03.md @@ -233,6 +233,7 @@ test/job/CustomerReminderJobIntegrationTest.java ✅ 9/9 绿 - **Standards 轴**:硬违规 0;5 Warnings → 修复 4:① `removeMember` 缺 `@Transactional`(oplog 写失败留半写)② `OpportunityMapper` `pageByCustomer`/`countActiveByCustomer` 漏 `o.deleted=0`(口径分裂,已补)③ `CustomerFocusServiceImpl implements CustomerConstants` 常量接口反模式(全仓首例,改静态引用)④ `TEMPLATE_NAME` 依赖实现类(上移 `ICustomerImportService`,Controller/Impl 同步切引用);不修 1:`pageWorkspace` 两分支 saved-view 片段重复(Shotgun Surgery,票 10 存量,留票 10 范围内动) - **Spec 轴**:不符②③④⑤与拍板口径逐项一致 ✅;票 12 十项 checklist 无漏项;API-SUMMARY 抽验 9 端点全一致 ✅;Warnings 2 → 均修:`removeMember`(同 Standards)+ §2.2 漏列 `release-pool-batch` 端点(实形态 `POST /release-pool-batch?ids=` → `BatchResult`,已补表) - **回归验证**:修复后重跑三模块 `test -pl crm-rule,crm-customer,crm-opportunity -o` **BUILD SUCCESS 521/0/0**(52 类:146/133/242 持平修复前,review 修复零破坏)+ 全仓 BOM 扫描 688 文件零命中 +- **跨机交接包**:`HANDOFF-CROSS-MACHINE-2026-09-03.md` + `tools/`(冒烟/回归脚本脱敏版,凭据走环境变量)已入库随 git 同步,换机器以该包为准 --- diff --git a/.scratch/customer-module/HANDOFF-CROSS-MACHINE-2026-09-03.md b/.scratch/customer-module/HANDOFF-CROSS-MACHINE-2026-09-03.md new file mode 100644 index 0000000..9456ebe --- /dev/null +++ b/.scratch/customer-module/HANDOFF-CROSS-MACHINE-2026-09-03.md @@ -0,0 +1,62 @@ +# 跨机器交接包(2026-09-03 会话收尾;随 git 同步,权威版) + +> 同源副本:本机 `%TEMP%\handoff-crm-customer-2026-09-03.md`(不随 git 走)。**换机器以本文件为准。** +> 交接基线:commit `dc8aada`(票 09-14 全交付)+ 本交接包提交。 + +## 0. 一句话状态 + +`.scratch/customer-module/` 交接档第 6 节顺序**全部完成**:票 12(原型逐页对齐 + 不符②③④⑤修复 + 冒烟)、票 13(API-SUMMARY.md)、票 14(code-review 两轴 + 6 项修复 + 回归 521/0/0 + 提交 `dc8aada`)。**无待办挂起**,新机器 pull 后可自由接新任务或处理 §4 遗留。 + +## 1. 交付索引(不复制内容,只给路径) + +| 交付 | 权威出处 | +|---|---| +| 票 12 十页对齐结论、不符②③④⑤实施细节、冒烟记录 | [HANDOFF-2026-09-03.md](../customer-module/HANDOFF-2026-09-03.md) §3.7.2 / §3.7.6 | +| 票 13 接口文档(9 Controller 55 端点 + 错误码 67001-67016 + 已知缺口 5 项) | `.scratch/customer-module/API-SUMMARY.md` | +| 票 14 code-review 两轴结果 + 6 项修复 + 回归验证 | `HANDOFF-2026-09-03.md` §3.9 | +| 票 09-14 全部代码/文档变更 | `git show dc8aada --stat`(75 文件 +5590/-195,33 新文件) | +| 踩坑 ①–㉕(历史累积) | `HANDOFF-2026-09-03.md` §4 踩坑表 | + +## 2. 新机器 bootstrap(按序) + +1. **代码**:`git pull` 到基线(`dc8aada` + 本交接包)。 +2. **先读**:`AGENTS.md`(JPA 孤立单大写字母规则、BOM 纪律、edit 工具降级阶梯)→ `HANDOFF-2026-09-03.md` §4 踩坑表。 +3. **工具链**:JDK + Maven(repo 根带 `settings.xml`;Maven 本体自备);可选 docker 部署看 `deploy/README.md`。 +4. **回归验证基线**: + ```powershell + mvn test -pl crm-rule,crm-customer,crm-opportunity -o + python .scratch/customer-module/tools/regress_check.py + # 期望:BUILD SUCCESS;BOM-SCAN bom_hits=0;SUREFIRE Tests=521 Failures=0 Errors=0 + ``` +5. **本地起服务**(冒烟需要):打 fat jar 或 IDE 跑 crm-app;⚠ 重打包前先停 8080 的 java 进程(jar 锁)。 +6. **冒烟**(可选,验证票 12 端点): + ```powershell + $env:CRM_TOKEN='<登录接口获取>'; $env:CRM_DB_HOST=''; $env:CRM_DB_PASSWORD='' + python .scratch/customer-module/tools/smoke-t12m.py # 全链:quick-create→focus/star/unstar/unfocus→占位/action + python .scratch/customer-module/tools/smoke-t12m2.py # star 逐拍断言(需 $env:CRM_SMOKE_CID) + ``` + 工具脚本凭据**一律走环境变量**(原 `tmp/` 版含明文 DB 密码,未入库)。knife4j:`http://localhost:8080/doc.html`。 + +## 3. 工具层经验(本会话高密度踩坑,动手前先看) + +1. **假失败真写入(本会话 8+ 次)**:Write/SearchReplace 报 `save file failed, reason: unknown` 但实际已落盘。纪律:报错先 Read/Grep 验证 → 已写入则跳过绝不重试(重试会双写)。 +2. **@Schema description 内嵌引号**(如 `显 "--"`):edit 工具 JSON 序列化层次不可控,用 python `chr(92)/chr(34)` 构造字节序列(`.scratch/fix_quotes.py` 范式)一次修 6 处;大块 CJK payload 一律「Write 到文件 + python 读取替换」。 +3. **PowerShell 陷阱**:无 `tail`/`&&`(用 `;`);mvn 后台输出截断时用 `tools/regress_check.py` 聚合 surefire 拿全量数字。 +4. **git porcelain 第一列语义**(`M `/`A ` = staged,` M`/`??` = 未暂存):验证特定目录未混入用 `git diff --cached --name-only | Select-String logs` 精确判定。 +5. **pymysql 逐拍断言**:默认 autocommit=False + REPEATABLE READ 冻结快照(坑㉕);`tools/smoke-t12m2.py` 范式 = 每步新连接 autocommit=True。 +6. **沙箱 Write 不能落工作区外**:外部目录(如 `%TEMP%`)走「工作区源文件 + Copy-Item(required_permissions)」。 + +## 4. 遗留事项(非阻塞,均已在交接档/API-SUMMARY 记录) + +1. **`logs/` 未被 .gitignore 忽略**且 `crm-app.log` 已 tracked——status 每会话有噪音。建议补 ignore + `git rm --cached`(用户未拍板,未动)。 +2. **pageWorkspace 两分支 saved-view 片段重复**(Shotgun Surgery)——票 10 存量,留票 10 范围内动(HANDOFF §3.9)。 +3. **QuickCreateDTO 无 confirmSimilar 字段**——快建命中相似客户无法「仍要创建」(票 02/03 范围;API-SUMMARY §5.1)。 +4. **详情汇总卡 5 字段恒 null / 页签 5·6 骨架**——grill Q18 契约前端显 "--",进行中商机总额可经 port 扩展切真值。 +5. 范围外划出项(敏感三档脱敏/合并/导出/联系人子表导入)见 API-SUMMARY §5。 + +## 5. Suggested skills + +- `/implement .scratch\customer-module` — 继续该模块工作(第 6 节已空,需先与新任务对齐) +- `/code-review` — 后续变更再评审(fixed point 用当前 HEAD) +- `/grill-me` — 新开专项(如遗留项 1-3 择一立项)先拍板 +- `/lanhu-bridge` — 原型对齐类工作(蓝湖页面读取路径见 AGENTS.md) diff --git a/.scratch/customer-module/tools/regress_check.py b/.scratch/customer-module/tools/regress_check.py new file mode 100644 index 0000000..5c7d488 --- /dev/null +++ b/.scratch/customer-module/tools/regress_check.py @@ -0,0 +1,30 @@ +"""回归汇总 + BOM 扫描(无敏感信息;在 repo 根目录执行)。 + +用法:python .scratch/customer-module/tools/regress_check.py +依赖:三模块已跑过 mvn test(surefire-reports 就位)。 +""" +import re +import glob + +# 1) BOM scan: all crm-* module java sources +bom_files = [] +total = 0 +for p in glob.glob('crm-*/src/**/*.java', recursive=True): + total += 1 + with open(p, 'rb') as fh: + if fh.read(3) == b'\xef\xbb\xbf': + bom_files.append(p) +print(f'BOM-SCAN: scanned={total} bom_hits={len(bom_files)}') +for b in bom_files[:20]: + print(' BOM:', b) + +# 2) surefire aggregate for the 3 regression modules +t = f = e = n = 0 +for mod in ('crm-rule', 'crm-customer', 'crm-opportunity'): + files = glob.glob(f'{mod}/target/surefire-reports/*.txt') + for p in files: + s = open(p, encoding='utf-8', errors='ignore').read() + m = re.search(r'Tests run: (\d+), Failures: (\d+), Errors: (\d+)', s) + if m: + t += int(m.group(1)); f += int(m.group(2)); e += int(m.group(3)); n += 1 +print(f'SUREFIRE modules=3 classes={n} Tests={t} Failures={f} Errors={e}') diff --git a/.scratch/customer-module/tools/smoke-t12m.py b/.scratch/customer-module/tools/smoke-t12m.py new file mode 100644 index 0000000..d1df52b --- /dev/null +++ b/.scratch/customer-module/tools/smoke-t12m.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +"""票 12 不符③④⑤冒烟(跨机器脱敏版;凭据一律走环境变量)。 + +必备环境变量: + CRM_TOKEN Bearer token(经 crm-auth 登录接口获取;不落任何文件) +可选: + CRM_BASE 默认 http://localhost:8080 + CRM_DB_HOST/CRM_DB_PORT/CRM_DB_USER/CRM_DB_PASSWORD/CRM_DB_NAME + 直连库做逐拍校验,默认 127.0.0.1:3306 / root / crm + CRM_SMOKE_CID 复用既有客户 id 跳过 quick-create(重复跑/换库时用) + +PowerShell 示例: + $env:CRM_TOKEN='...'; $env:CRM_DB_HOST='...'; $env:CRM_DB_PASSWORD='...' + python .scratch/customer-module/tools/smoke-t12m.py +""" +import json +import os +import time +import urllib.parse +import urllib.request + +BASE = os.environ.get('CRM_BASE', 'http://localhost:8080') +token = os.environ.get('CRM_TOKEN') +assert token, '缺 CRM_TOKEN 环境变量(Bearer token,经登录接口获取)' + + +def call(method, path, form=None): + url = BASE + path + headers = {'Authorization': 'Bearer ' + token} + body = None + if form is not None: + body = urllib.parse.urlencode(form).encode('utf-8') + headers['Content-Type'] = 'application/x-www-form-urlencoded' + req = urllib.request.Request(url, data=body, headers=headers, method=method) + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read().decode('utf-8')) + + +import pymysql +conn = pymysql.connect(host=os.environ.get('CRM_DB_HOST', '127.0.0.1'), + port=int(os.environ.get('CRM_DB_PORT', '3306')), + user=os.environ.get('CRM_DB_USER', 'root'), + password=os.environ.get('CRM_DB_PASSWORD', ''), + database=os.environ.get('CRM_DB_NAME', 'crm'), charset='utf8mb4') + + +def db_focus(cid): + cur = conn.cursor() + cur.execute("SELECT COUNT(*), IFNULL(SUM(starred), -1) FROM customer_focus WHERE customer_id = %s", (str(cid),)) + row = cur.fetchone() + print(' db customer_focus rows=%s starred_sum=%s' % (row[0], row[1])) + return row + + +cid = os.environ.get('CRM_SMOKE_CID') +if not cid: + r = call('POST', '/api/customer/quick-create', { + 'customerName': '星辰大海贸易有限公司' + time.strftime('%H%M%S'), + 'customerType': 'TYPE_A', + 'provinceCode': '310000', + 'cityCode': '310100', + 'industryCode': 'I', + 'customerStarLevel': '3', + 'relationStarLevel': '2', + 'confirmSimilar': 'true', + }) + print('quick-create code:', r.get('code')) + cid = (r.get('data') or {}).get('id') + if not cid: + print('quick-create response:', json.dumps(r, ensure_ascii=False)[:600]) + raise SystemExit('ABORT: no cid (dup-check hit? 换 CRM_SMOKE_CID 复用或改名重跑)') +print('cid:', cid) + +# 67002 校验:客户不存在 +r404 = call('POST', '/api/customer/999999999/focus') +print('focus unknown-cust code (expect 67002):', r404.get('code')) + +# ③ focus 幂等 +print('focus#1 code:', call('POST', '/api/customer/%s/focus' % cid)['code']) +db_focus(cid) +print('focus#2 code:', call('POST', '/api/customer/%s/focus' % cid)['code']) +db_focus(cid) + +# star 幂等(含已关注行原位置 1) +print('star#1 code:', call('POST', '/api/customer/%s/star' % cid)['code']) +db_focus(cid) +print('star#2 code:', call('POST', '/api/customer/%s/star' % cid)['code']) +db_focus(cid) + +# focus-batch 幂等(批量传自身) +rb = call('POST', '/api/customer/focus-batch?' + urllib.parse.urlencode({'ids': '%s,%s' % (cid, cid)})) +print('focus-batch code:', rb.get('code')) +db_focus(cid) + +# unstar(保留行) +print('unstar code:', call('POST', '/api/customer/%s/unstar' % cid)['code']) +db_focus(cid) + +# unfocus(物理删行) +print('unfocus code:', call('POST', '/api/customer/%s/unfocus' % cid)['code']) +db_focus(cid) + +# ④ detail-head 占位字段恒 null +d = call('GET', '/api/customer/%s/detail-head' % cid)['data'] +keys = ['wonProjectAmount', 'planEstimateAmount', 'ongoingOpportunityAmount', 'contractAmount', 'paidAmount'] +print('detail-head placeholder nulls:', {k: d.get(k) for k in keys}) + +# ⑤ oplog action 过滤 +op_all = call('GET', '/api/customer/%s/oplog/page' % cid)['data'] +op_c = call('GET', '/api/customer/%s/oplog/page?action=CREATE' % cid)['data'] +op_u = call('GET', '/api/customer/%s/oplog/page?action=UPDATE' % cid)['data'] +print('oplog all total=%s actions=%s' % (op_all['total'], [row['action'] for row in op_all['content']])) +print('oplog action=CREATE total=%s' % op_c['total']) +print('oplog action=UPDATE total=%s (expect 0)' % op_u['total']) + +conn.close() +print('SMOKE DONE') diff --git a/.scratch/customer-module/tools/smoke-t12m2.py b/.scratch/customer-module/tools/smoke-t12m2.py new file mode 100644 index 0000000..b18045c --- /dev/null +++ b/.scratch/customer-module/tools/smoke-t12m2.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +"""票 12 不符③冒烟补充:star 中间态逐拍验证(每步新连接,避开 REPEATABLE READ 快照冻结,坑㉕)。 + +必备环境变量: + CRM_TOKEN Bearer token(经 crm-auth 登录接口获取) + CRM_SMOKE_CID 目标客户 id(任意可操作客户;或先跑 smoke-t12m.py 自动建) +可选: + CRM_BASE 默认 http://localhost:8080 + CRM_DB_HOST/CRM_DB_PORT/CRM_DB_USER/CRM_DB_PASSWORD/CRM_DB_NAME 同 smoke-t12m.py + +PowerShell 示例: + $env:CRM_TOKEN='...'; $env:CRM_SMOKE_CID='...'; $env:CRM_DB_PASSWORD='...' + python .scratch/customer-module/tools/smoke-t12m2.py +""" +import json +import os +import urllib.request + +BASE = os.environ.get('CRM_BASE', 'http://localhost:8080') +token = os.environ.get('CRM_TOKEN') +assert token, '缺 CRM_TOKEN 环境变量(Bearer token,经登录接口获取)' +CID = os.environ.get('CRM_SMOKE_CID') +assert CID, '缺 CRM_SMOKE_CID 环境变量(目标客户 id,可先跑 smoke-t12m.py 自动建)' + + +def call(method, path): + req = urllib.request.Request(BASE + path, headers={'Authorization': 'Bearer ' + token}, method=method) + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read().decode('utf-8')) + + +def db_row(tag): + import pymysql + conn = pymysql.connect(host=os.environ.get('CRM_DB_HOST', '127.0.0.1'), + port=int(os.environ.get('CRM_DB_PORT', '3306')), + user=os.environ.get('CRM_DB_USER', 'root'), + password=os.environ.get('CRM_DB_PASSWORD', ''), + database=os.environ.get('CRM_DB_NAME', 'crm'), charset='utf8mb4', autocommit=True) + cur = conn.cursor() + cur.execute("SELECT COUNT(*), IFNULL(SUM(starred), -1) FROM customer_focus WHERE customer_id = %s", (CID,)) + row = cur.fetchone() + conn.close() + print('%-12s rows=%s starred_sum=%s' % (tag, row[0], row[1])) + return row + + +print('focus :', call('POST', '/api/customer/%s/focus' % CID)['code']) +db_row('after-focus') +print('star :', call('POST', '/api/customer/%s/star' % CID)['code']) +r = db_row('after-star') +assert r[1] == 1, 'FAIL: starred should be 1 after star' +print('unstar:', call('POST', '/api/customer/%s/unstar' % CID)['code']) +r = db_row('after-unstar') +assert r == (1, 0), 'FAIL: row kept with starred=0 expected, got %s' % (r,) +print('unfocus:', call('POST', '/api/customer/%s/unfocus' % CID)['code']) +r = db_row('after-unfocus') +assert r[0] == 0, 'FAIL: row should be physically deleted, got %s' % (r,) +print('STAR-SMOKE ALL PASS')