开发日志_2026-11-01.md 24 KB

2026-11-01 诊断规则重做(v2)

起点:用户反馈

「比起前端 UI 样式,门店检测的逻辑更需要调整。现在的门店诊断结果并不严谨, 诊断建议也不是根据门店实际问题给出的,而是一股脑全给。」 要求:先做思维导图统一梳理「每项指标没达到的问题 + 整改建议」,确认后再写进代码。

第一步:用数据定位不严谨在哪

对 1409 家门店实测,找出 5 个硬伤:

问题 实测 结论
核销金额基准 10000 当不达标线 全品牌中位 165 元,仅 8% 达标 命中 1023 家 = 噪音,不是诊断
回复率 <50% 命中 1305 家(93%) 没有区分度
评分 0 分 91 家被判「低于 4.3」 0 分是未开分,不是评分低
差评率小样本 22 家「1-2 条评价」就判 一条差评 = 非常严重
整改动作 9 条固定清单,每家全给 用户说的「一股脑全给」

补充发现(重要,推翻了之前的判断):

  • 源头 CSV 没有一个空单元格,所有缺失都是字面 0 → 不是「导入丢了空值」,而是平台用 0 表示「未发生/未开分」,代码当成了真实值
  • 「待核销 = 意向成交 − 核销」在同源数据里 868 家会算出负数(没扣退款) → 文案里写的「真实未核销」是错的,已改成「估算未核销」

第二步:思维导图(用户确认后才动代码)

产出:

  • 项目记录/规则设计_v2_思维导图.html(自包含,可打印)
  • 项目记录/规则设计_v2_明细.md(同一份规格的表格版)
  • 生成脚本按规格统一生成,文档与代码同源

结构:4 层漏斗(流量/转化/履约/口碑)× 16 条规则,5 档分级,1 棵诊断树,≤3 条动作。

第三步:用户拍板的 4 个决定

  1. 核销金额保留为考核项,但提高门槛 → 只在「核销率已达标」时才判客单价, 问题性质从「金额不够」变成「客单价低」,命中 1023 → 287 家
  2. 差评率小样本仍然判,但封顶预警线 → 22 家从「非常严重」降为「预警线」,不再有 1 条差评判 R3
  3. 评分 0 分仍算不达标,文案改「未开分」 → KPI 总数 323 不变,但文案与动作分开
  4. 诊断树按漏斗顺序 → 违规 → 流量 → 转化 → 履约 → 口碑

第四步:写进代码

backend/rules.py(重写)

  • DEFAULT_THRESHOLDS 新增 7 个可调门槛:visit_low / cvr_low / pend_high / hx_min_deal / amt_min_coupons / cp_min_new / cp_min_total / reply_min_new
  • 16 条规则,每条含 档位/层/级别/模板键/变量
  • diagnose() 返回 problems / primary / secondary / actions / sev / tier / headline / hd_key
  • 根因去重用 if/elif 链保证互斥:零成交 → 成交未核销 → 核销率低 → 客单价低
  • bad_rate() 改为返回 (ratio, 口径, 是否小样本),小样本在 diagnose 里封顶 R1
  • 保留 detect() 兼容旧调用
  • 新增 V1_TEXTS:v1 默认文案快照,用于迁移

backend/db.py

  • get_thresholds() 用默认值补全库里没有的新键(老库零迁移)
  • get_texts() 迁移:存档值 == v1 默认值 → 用新默认;否则视为用户自定义保留 (实测保留了用户的 3 处自定义:概览行模板、访问行模板、传素材动作文案)
  • aggregate_period() 补输出 reviews_new/bad_new/reviews_total/bad_total/lit —— 差评率判定需要原始计数,之前只算了一个 cp_rate 出来

backend/server.py

  • payload 增加 tier / headline / hd_key / primary / secondary / actions / problems
  • 保留 flags 字段(列表页、标签渲染继续可用)
  • 导出 CSV 增加「诊断结论 / 主因 / 全部问题 / 整改动作」四列, 整改动作只给诊断树的 ≤3 条

frontend/app.js

  • diagnosisOf(s, X):用当前(可能未保存的)模板重新渲染问题与动作
  • renderFeedback() 重写:概览 → 结论 → 主因+次要 → 待核销 → 流失 → 洞察 → ≤3 动作
  • 删除 previewFlags()(前端复刻的第二套判定规则), 改为直接用后端 problems + 模板键重渲染 → 规则唯一源真正落到 rules.py
  • 抽屉改为「结论横幅 + 主因 + 次要 + 指标条 + 折叠的其余提升点」
  • 报告同步;【指标配置】面板接入 7 个新阈值
  • 文案配置面板从 43 个字段扩到 65 个(新增 hd_* 结论句、新问题文案、新动作)

验证

  • 规则命中数与原型脚本完全一致(LIT 867 / REPLY 822 / HX 695 / CP 349 / AMT 287 …)
  • 主因分布:违规 469、核销率低 417、客单价 185、访问不足 101、成交未核销 81 …(13 种主因)
  • 平均问题 3.15 条、平均动作 2.15 条(≤3 上限生效)
  • Node harness 全量 1409 家:残留变量 0、动作超 3 条 0、有问题无主因 0
  • 报告渲染全量 1409 家:0 报错
  • 导出接口 16 列、469 行正常

已知遗留

  • 「待核销」仍是估算值(缺后台真实未核销列)。要根治需要数据源提供该列。
  • 回复率命中 822 家(58%)仍偏高,因为品牌确实 58% 的门店有新评价但没回复; 阈值 reply_min_new 可在面板调到 5,命中会降到 414 家。
  • 洞察段仍是 6 条全列,未与主因联动;如需可下一步收敛为「主因相关的 2-3 条」。

第二轮:按用户编辑的可视化规则图落地(v2.1)

输入

用户在 规则全景图_可编辑.html 里改了 11 条规则、新增 1 条、删除 2 条, 导出 规则修改_2026-11-01.json。已用浏览器实时状态核对,导出的就是最终版。

用户的改动

类型 内容
删除 VISIT0(并入 VISIT,判定改为「访问 < 100」含 0)、SCORE0(并入 SCORE)
新增 新增评价低(新增评价 < 10 条,T 级)
阈值 视频 0 → <10、点亮 0 → <10、转化率 2% → 10%
重命名 客单价偏低 → 核销金额偏低;退单率超线 → 商责退单率超线
重写判定 核销金额去掉「核销率已达标」前置条件
文案 12 条问题的动作全部改写为更具体的门店执行动作

两个需要我定的点(用户确认)

  1. 核销金额独立判定 —— 只要金额 < 10000 就报,不管核销率(命中 1299 家 / 92%)
  2. 问题全列、每条给动作 —— 不再截断到 3 条

代码改动

  • rules.py
    • 规则表 15 条;DIAGNOSIS_ORDER 去掉 VISIT0/SCORE0,加入 RVOL
    • 核销链改为互斥 if/elif,AMT 移出链外独立判定
    • SCORE 一条规则内区分 0 分(走 t_SCORE0「未开分」文案)
    • 动作不再截断;新增 _clean_action() 去掉动作行首的 ✅,避免与「✅ {动作}」模板叠加
    • 新增阈值 video_low / lit_low / new_review_low,cvr_low 默认 2 → 10
    • hd_key 回退链:规则 key → 模板 key(区分未开分)→ 层 → OK
  • db.py
    • set_texts() 改为只存与默认值不同的键 —— 以后改默认文案不会再被旧存档盖住,也不再需要 V1 快照比对(保留 V1 兼容历史存档)
  • 前端
    • diagnosisOf 的动作覆盖全部问题;风险段、抽屉、报告全部改为「问题全列」
    • 阈值面板接入 3 个新参数;文案面板移除 VISIT0、加入 RVOL

踩坑

  • 用 str.replace 批量改文案时误伤了 V1_TEXTS 快照(t_VISIT / t_VIDEO 两处字符串完全相同), 导致迁移判断失效、旧值被当成用户自定义。已按 V1 真实值修复,并改为「只存差异」从根上避免这类问题。
  • 用户把转化率判定写成 10%,但参数 cvr_low 还是 2% —— 条件文本和参数是两处,已按 10% 统一。

当前命中(2026-10,1409 家)

规则 命中 规则 命中
点亮偏低 1305 (93%) 评分不达标 323 (23%)
核销金额偏低 1299 (92%) 未核销积压 253 (18%)
转化率低 1213 (86%) 成交未核销 221 (16%)
新增评价少 1170 (83%) 商责退单率 165 (12%)
回复率偏低 822 (58%) 访问过低 147 (10%)
核销率不达标 695 (49%) 零成交 56 (4%)
视频量少 437 (31%) 投诉率 11 (1%)
差评率超线 349 (25%)

平均每家 6.01 条问题 / 6.01 条动作,单店最多 11 条。

待观察

  • 主因有 57%(798 家)落在「转化率低」,因为 CVR 阈值放到 10% 后命中 86%,且诊断树排第 4 位。 若觉得结论太单一,可把 CVR 在诊断树里往后挪,或把阈值收回到 5% 左右。
  • 洞察段 6 条与问题列表内容重复度高,可考虑收敛。

第三轮:近30天滚动窗口(快照模式)

需求

用户要求:每天从抖音来客「报表集市」导出近30天数据并入库,分析口径围绕近30天滚动窗口。

已发现的问题(动手前先验证)

把「按统计周期」的导出文件直接丢进旧版导入,会静默写错: 统计日期是 2026-10-03-2026-11-01 这种区间字符串,旧代码只截取开头 2026-10-03, 结果 30 天的汇总被当成「10 月 3 日一天」的日报,窗口结束日丢失。 (测试时误把 2 家测试门店写进真实库,已清理,KPI 复核无异常。)

已完成:快照数据模型

  • metrics 新增 window_start / window_end,granularity 增加 snapshot
  • 老库自动迁移(PRAGMA 检查 + ALTER TABLE),无需手工操作
  • ingest.parse_range() 识别 2026-10-03-2026-11-01 / ~ / 至 / / 分隔等写法
  • 区间 → period 记「截至日」、每店一行、granularity='snapshot'
    • 首尾同一天 → 自动退化为单日日报,不会误判
  • get_periods() 返回快照标签「近30天 · 截至 11-01」并带窗口区间
  • aggregate_period() 快照的「数据天数」按窗口长度算
  • 前端周期下拉显示快照标签,顶部数据说明显示统计区间
  • 月汇总数据与快照共存,互不影响

验证:隔离副本连续导入 11-01 / 11-02 两份快照,周期列表、聚合、窗口字段全部正确。

进行中:生意经导出自动化

已确认的事实:

  • ego lite 已登录生意经(河南怡呵美水光企业管理有限公司)
  • 平台会记住上次勾选的维度和指标,进门店 tab 后自动勾选 47 项,无需每天重选
  • 建任务有每日次数限制(当时显示「今日剩余 29 次」)
  • 下载列表是 Bytedance 表格,每行有「下载」按钮

卡点:日期区间控件(byted-date-picker)点选不生效。 试过合成 click、真实 click、标记后 click,日格能拿到 byted-date-selected 样式但输入框值不变; 面板里存在多个 popper,疑点是我抓到的不是输入框对应的那一个。下一步需要重新定位 popper 与输入框的绑定关系。

尚未创建任何下载任务,未消耗配额。

生意经接口已打通(关键进展)

放弃点 UI,改为抓接口。找到的统一网关:

POST https://www.life-data.cn/api/lowcode_api/query
     ?projectKey=shengyijing&pageId=15352&interface_id=0ac0bacd

必带的请求头(少了会返回 code -1):

content-type: application/json; charset=utf-8
root-life-account-id: 7376197379193669659
life-account-id:      7376197379193669659
x-tt-trace-log: 01

请求体(创建「按日 × 日期区间 × 全部门店」的导出任务):

{"interface_id":"0ac0bacd","data_query":{
  "data_query_type":"data_query",
  "indicators":[ 41 个字段,poi_id / enter_poi_cnt / verify_cert_cnt_1d / reply_rate_ratio ... ],
  "filters":{"operator":"and","filters":[
    {"field":{"name":"date"},"filter_operator":"between","range_values":["20261003","20261101"]},
    {"field":{"name":"poi_selector_json"},"filter_operator":"equal","value":"{全部门店选择器}"},
    {"field":{"name":"ignore_zero_rows"},"filter_operator":"equal","value":1}]},
  "group_by":["day"],
  "order_by":[{"order_field":"day","order_type":"desc"}],
  "extra":{}}}

返回:{"code":0,"status":"submit_success","task_id":"431b88fb-..."}

意义:日期区间完全由我们控制,彻底绕过了点不动的日历控件。

已捕获的其他接口:

  • interface_id=d715cc19 —— 按 query_task_name 查任务(可用于轮询状态)
  • interface_id=9e029004 —— 查 total / available(当日剩余建任务次数)
  • interface_id=0ac0bacd + data_query_type=meta_query —— 拉字段元数据(64 个字段)

待办

  1. 轮询任务状态 → 拿下载地址(或找到能直接返回数据行的查询接口,连文件都不用下)
  2. 把 JSON/文件转成 ingest 能吃的格式,落到 data/inbox/
  3. 每日定时(launchd 或 server 内置线程)

清理

测试期间创建过 2 个任务(「同步测试_1101」「报表集市_门店」),均已撤销, 下载列表恢复为原来的 2 条已完成任务。

同步链路跑通(端到端验证通过)

关键结论

  1. 导出文件是 .xlsx(不是 zip),现有导入器直接支持
  2. 下载地址由任务接口返回的 download_link 给出,是签名 URL,有效期约 1 天
  3. Python 直连被 CDN 挡(403 空响应,缺 secsdk 令牌),走 ego 页面上下文调接口可行
  4. 从建任务到出下载地址约 3.5 分钟,脚本里轮询上限 10 分钟

完整链路(已实测)

sync/run_sync.sh
  → ego 打开生意经(复用登录态)
  → POST interface_id=0ac0bacd 建任务(group_by=[],日期=今天-29~今天)
  → 轮询 interface_id=d715cc19 拿 download_link
  → page.fetch 下载 xlsx 到 data/inbox/门店数据_<start>_<end>.xlsx
  → 服务端 watcher 20 秒内导入
  → 文件名里的日期区间被识别 → 按快照入库 → 周期下拉出现「近30天 · 截至 11-01」

新增文件

  • sync/life_sync.mjs —— 同步主脚本(建任务/轮询/下载)
  • sync/run_sync.sh —— 定时调用入口(带日志与失败标记)
  • sync/com.yhm.lifesync.plist —— launchd 每日定时模板
  • ingest.range_in_filename() —— 文件名带日期区间时自动按快照入库
  • ingest 支持显式 window=(start,end) 参数

踩坑:一次真实的误写与修复

第一次同步时服务还在跑旧代码,没加载窗口识别,结果:

  • 30 天数据被当成「2026-10 月度汇总」,覆盖了原有 1409 家月度数据 修复:备份库 → 清掉污染行 → 重导原始 门店数据_10月.csv 恢复月度 → 再把 xlsx 按快照重导 修复后:2026-10 月度 1409 家 + 2026-11-01 快照 1413 家,两者共存 教训:改完 ingest 必须重启服务再触发收件箱导入。

当前状态

  • 快照周期「近30天 · 截至 11-01」1413 家,已是工作台默认周期
  • KPI(该窗口):469→467 / 323→327 / 929→926 / 276→266
  • 今日建任务配额剩余 26 次(同步 2 次 = 测试)

定时任务:launchd 走不通,改成服务内置调度

先试了 launchd(失败)

配好 ~/Library/LaunchAgents/com.yhm.lifesync.plist(每天 01:00),launchctl load 成功、日历项注册成功, 但触发后报 /bin/bash: …/run_sync.sh: Operation not permitted。

逐步排查:

  1. 空环境 env -i 直接跑脚本 → 正常,不是 PATH 问题
  2. launchctl submit 探针 cd + ls → 成功,不是目录不可达
  3. 探针改用 head 脚本路径 → 也被拒,head=1

结论:macOS TCC 保护 ~/Documents,系统级 launchd 任务读不到这个目录里的文件内容。 要绕过必须在「系统设置 → 隐私与安全性 → 完全磁盘访问」里手动把 /bin/bash 加进去。 已卸载并移除该 LaunchAgent,plist 留在 sync/ 下作参考。

改成服务内置调度(可用)

工作台服务本身是从终端启动的,具备该目录的读权限,所以把定时器放进服务里:

  • server.daily_sync_loop() 线程:算到下一个 01:00,睡到点再跑
  • 启动补跑:距上次成功超过 20 小时 → 启动 90 秒后自动补一次(解决关机/休眠错过)
  • POST /api/sync 手动触发,GET /api/sync/status 查状态
  • 左侧栏新增【同步数据】按钮,点完每 5 秒轮询状态,完成后自动刷新数据
  • last_sync 时间戳存 settings 表

实测

  • 启动补跑:21:11:01 触发 → 21:15:33 成功,下载 298KB,入库 1413 家
  • 批次记录的周期已修正为 2026-11-01(之前误记成 2026-11)
  • 启动器加 -u,服务窗口能实时看到同步进度

当前配置

  • 每天 01:00 自动同步
  • 快照周期「近30天 · 截至 11-01」1413 家,为工作台默认周期
  • 月度 2026-10 数据 1409 家保留

阈值改面板 = 直接改代码(v2.2)

需求

用户:「我想要的是在【指标配置】面板改数字,就会直接同步把规则逻辑一起修改」 —— 不要「代码默认值」和「库里自定义值」两套数字打架。

实现

  • 新增 backend/codesync.py
    • write_defaults(T):把新阈值写回 rules.py 的 DEFAULT_THRESHOLDS 只替换已有键的「值」,保留键名/缩进/行尾注释/其它代码;原子写入 + 生成 .bak
    • 写完后清字节码缓存并 importlib.reload(rules),立即生效
    • get_factory():首次运行时把出厂值快照到 data/factory_thresholds.json
  • PUT /api/thresholds → 写库 + 写回代码
  • POST /api/thresholds/reset → 回到出厂值 + 同样写回代码
  • 面板语义改为对照出厂值:显示「出厂值 / 已自定义」
  • 保存成功提示区分「已生效,并同步写回代码默认值」/ 写回失败原因

规则地图跟随

gen_editable_map.py 里所有判定条件中的数字改为从 rules.DEFAULT_THRESHOLDS 读取, 改阈值后重新生成,图上文字自动同步(不再写死「≥30 张」)。

踩了两个坑

  1. 正则分组用错,把旧值和新值拼在一起,"score_base": 4.34.3、"pend_high": 3030, 直接把 rules.py 写坏。靠事前的 .bak 备份恢复。 教训:改源码的函数必须先在隔离副本里跑一遍,并且必须校验语法(ast.parse)再落盘。
  2. macOS 自带 Python 把 .pyc 放在 ~/Library/Caches/com.apple.python, 不在项目里的 __pycache__。删 __pycache__ 无效,同尺寸同秒的改动会被旧字节码顶掉 (现象:源码是 10,导入却是 15)。已在 write_defaults 里主动清理该缓存路径。

验证

连续 PUT 12 → 10 → 8 → 10,每次都用一个新进程从磁盘重新导入核对: 代码默认值、服务内默认值、源码三者完全一致。

UI:数据瘦身 + 放大字号 + 滚动加载

起因

用户反馈「排版太紧凑,字体也小」,同时问「滚动加载会不会每次刷新渲染太多」。

量了一下发现真正的问题不是渲染,是每次刷新要传 6.4 MB: 同一份问题数据被下发了 4 遍(problems / flags / primary / secondary / actions)。

1. 数据瘦身:6.4 MB → 1.9 MB(降 70%)

  • 后端只下发「模板键 + 变量」:{k,t,l,v,c,p,a,x},文案由前端按当前模板渲染
  • 删掉 flags / primary / secondary / actions / cats(都可从前端派生)
  • 前端新增 decorateStore():加载后一次性还原成原来的结构,其余代码零改动
  • 导出接口 /api/export.csv 改为按模板现场渲染(内容与原来一致)
  • KPI、分布统计、分类筛选改用 tier / problems 派生

2. 放大字号与间距

  • 基础字号 13.5 → 15px;12.5/11.5/10.5 档整体上调一档(现在 14/13/12)
  • 表格单元格内边距 11px → 14px,表头 10px → 12px
  • 卡片内边距 18/20 → 22/24;图表高度 222 → 290
  • 收窄的 .wrap 最大宽 1320 → 1400,两侧留白加大

3. 首屏 20 条 + 滚到底加载

  • FIRST_ROWS=20、STEP_ROWS=20、MAX_ROWS=300
  • 去掉上一页/下一页,改成状态行「已显示 20 / 1413 家,继续下滑加载更多」
  • 滚到距底部 600px 自动追加;到 300 条上限后提示用筛选缩小范围
  • 冒烟测试:首屏 20 行、加载一次 40 行、300 行渲染 1ms / 157KB HTML

未做

  • 密度切换开关(先按「舒适」档定死,用户看效果后再说)

后续:改回分页

用户反馈滚动加载不合适,改回分页:

  • PAGE_SIZE = 10,恢复上一页/下一页,状态行「第 1 / 142 页」
  • 移除 loadMore / nearBottom / 滚动监听 / 300 条 DOM 上限
  • 数据瘦身和字号放大保留 验证:第 1 页 10 行、第 2 页 10 行、末页(142)3 行,按钮禁用状态正确。

排序控件拆成两个下拉

  • 原因:原来 5 个选项把「字段」和「方向」写死在一起(如「按核销金额(低→高)」,方向不可切换)
  • 现在:#fsort 只选字段(严重度/核销金额/评分/核销率/访问量),#forder 选方向(倒序/正序)
  • 默认:严重度 + 倒序(与原来一致)
  • 空值(未开分、无核销率)无论正序倒序都排最后,不参与翻转
  • 同值时按核销金额兜底排序
  • 验证:5 字段 × 2 方向共 10 种组合,排序结果均正确

列表增加列(7 列 → 13 列)

原来只有 评分/核销率/核销金额/退单率,漏了 R 级违规里的投诉率、差评率,也看不到流量和履约。

新增 6 列:访问 / 客资(意向成交券)/ 待核销 / 投诉率 / 差评率 / 回复率

配套调整:

  • 超标的值直接标红(R 级红 #b5563a,未达标金 #c8952c),扫一眼就能定位
  • 评分 0 分显示「未开分」而不是 0.0;差评率无评价显示「无评价」,不参与排序翻转
  • 「触发问题」列原来显示完整问题文案(含数字),与新增列重复且把行高撑到 127px, 改为显示问题分类短名(差评率超线 / 评分不达标 / 成交未核销…),最多 5 个 + 「+N」
  • 列宽收紧:单元格内边距 14/12 → 13/10,标签字号 12 → 11.5
  • .tablewrap{overflow-x:auto} 兜底,当前 1274px < 容器 1292px,不出现横向滚动条
  • 行高 127px → 93px

口径变更:核销率分母改用「门店页成交券数」

需求

用户:「门店页成交券数,现在统一改成这个。包括核销率」 —— 原来看板上的「客资」列其实是门店意向成交券数(intend_deal),用户要的是门店页成交券数(deal)。

改动范围(全链路统一)

位置 原来 现在
核销率 核销券 ÷ 意向成交券 核销券 ÷ 门店页成交券数
待核销 意向成交券 − 核销券 成交券数 − 核销券
零成交判定 意向成交券 = 0 成交券数 = 0
成交未核销 意向成交券 > 0 且 核销 = 0 成交券数 > 0 且 核销 = 0
核销率样本门槛 意向成交券 ≥ 5 成交券数 ≥ 5
转化率分子 成交券数 不变(本来就是这个)
返单/投诉率加权 按意向成交券加权 按成交券数加权
文案变量 {客资} {成交券数} 意向成交券数 门店页成交券数
表格「客资」列 意向成交券数 改名「成交券」,取成交券数
报告数据卡 门店意向成交券数 门店页成交券数

影响(2026-11-01 数据)

  • 核销率 < 40% 的门店:926 → 910 家(分母变小,核销率整体略升)
  • 中位核销率:27.8% → 28.1%
  • 核销率 > 100% 的门店:4 → 15 家(成交券数可能小于核销券数,因为核销的是更早周期的券)—— 属正常现象,不做截断
  • 其他 KPI 不变:R 467 / 评分 327 / 零核销 266

备注

  • 数据库里两个字段都保留(deal 与 intend_deal),只是判定与展示统一用 deal
  • 文案模板里用户写的 {门店意向成交券数} 通过别名仍然有效,但取的是成交券数的值