Commit Graph

251 Commits

Author SHA1 Message Date
guke 63daeeaf9b fix(compare-alert): cancelled trace 可读但无卡点时回退耗时兜底(超长放弃不再漏报)
线上 trace 几乎总可读,旧逻辑「可读但没判出卡点 → 不报」使 total_ms>90s 阈值形同
虚设,超长放弃(实测 113s / 516s)一条都报不出。改为只有判出卡点才独占带卡点的 T5,
其余(可读没卡点 / 读不到 / 无 trace)一律回退耗时/步数兜底,超长照报(卡点列留空)。
同步更新 stuck-detection 设计文档 6.1 + 修订说明。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 13:52:48 +08:00
guke 523d970c45 chore(compare-alert): 扫描间隔默认 30min→15min + 同步 test_defaults
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 17:09:45 +08:00
guke 44beb3b8de Merge branch 'main' of https://gitea.shaguabijia.com/WonderableAI/shaguabijia-app-server into feat-compare-fail-alert
# Conflicts:
#	app/admin/repositories/queries.py
2026-08-05 17:03:48 +08:00
guke 62b30342ed refactor(compare-alert): 补 _fmt_stuck 类型注解 + 卡片 docstring 列序 + failed 卡点注释
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 16:51:06 +08:00
guke 347c4c7de4 feat(compare-alert): 卡点独立成列(AlertHit.stuck_point + 卡片第5列),reason 去重简化
- AlertHit 加 stuck_point: str | None = None 字段(格式化好的「平台·环节 帧/s」)
- classify_cancelled_fallback reason 简化为「深度放弃」(耗时/步数已在「用时」列,不重复)
- build_hits 加 _fmt_stuck helper;cancelled 卡死 stuck_point=「美团·加菜 110帧/32s」;
  failed stuck_point=环节标签、reason 不再附「卡在 X」
- format_alert_card 列序改为 时间/手机号/用时/失败原因/卡点/版本/trace (7列)
- 同步更新 test_compare_alert_stuck_worker / _fallback / _format / _rules 断言

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 16:47:23 +08:00
guke e135ba9a84 fix(compare-alert): stuck_ms 负值(时钟回退)降级为 None
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 16:33:30 +08:00
guke dd96fc2151 feat(compare-alert): StuckPoint 加 stuck_ms(末段卡住时长,读帧 timestamp)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 16:27:07 +08:00
guke fed3541a51 refactor(compare-alert): 抽 _build_card 消除卡片三路径重复 + grouped 显式化
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 15:47:49 +08:00
guke 924e40a84e feat(compare-alert): 固化飞书卡片 table 格式(format_alert_card + send_feishu_card),worker 切换
- feishu_notifier: 新增 send_feishu_card(interactive msg_type,复用 _post_feishu)
- compare_alert_format: 新增 format_alert_card(schema 2.0, header red, markdown摘要+table 6列)
  - 列序: 时间/手机号/用时/失败原因/版本/trace(lark_md);无 width 属性
  - cost 列 helper _cost_cell: total_ms→Ns / step_count→M步 / 两者用" / "连 / 都无给"-"
  - 截断: 超 max_total 只出摘要; 空 hits 返回「本期无异常」卡片
  - 保留 format_alert_message / format_alert_post(有测试依赖)
- compare_alert_worker: _send(post) → _send_card(card); _scan_and_alert 调 format_alert_card
  - SEND_EMPTY 分支: 传空 hits 给 format_alert_card 得「本期无异常」卡片
  - webhook 空降级保留; build_hits/水位逻辑不动
- tests: format/feishu/worker 测试全适配新接口,86 passed 零回归
- 删除临时脚本 scripts/_test_alert_card.py

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 15:37:13 +08:00
guke 9598c7a1da feat(compare-alert): AlertHit 加 total_ms/step_count(卡片用时列数据源)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 15:27:47 +08:00
guke 0663ee5542 fix(compare-alert): worker import 排序 + build_hits 返回类型 + 共享预算测试
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 14:52:47 +08:00
guke 45a8e7b972 feat(compare-alert): worker 编排 build_hits(cancelled trace 优先+保底、failed 附卡点)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 14:41:13 +08:00
guke 02d2e56ef6 feat(compare-alert): 抽出 classify_cancelled_fallback + 公开 make_hit
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 14:32:53 +08:00
guke a7e8141497 fix(compare-alert): trace_stuck _read_head 防损坏帧 UnicodeDecodeError 崩溃
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 14:29:25 +08:00
guke c930957e90 feat(compare-alert): trace_stuck 卡死判据(末段原地打转)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 14:19:53 +08:00
guke 6c143dc9f2 feat(compare-alert): 卡死定位 4 个配置项
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 14:14:06 +08:00
zuochenyong b39d918cda feat(提现): 新增 100 元档并向客户端下发每档每日次数上限 (#218)
Co-authored-by: exinglang <exinglang@qq.com>
Reviewed-on: #218
Co-authored-by: zuochenyong <zuochenyong@wonderable.ai>
Co-committed-by: zuochenyong <zuochenyong@wonderable.ai>
2026-08-05 13:47:19 +08:00
guke 4becde8d75 fix(compare-alert): 开关/webhook 默认值加注释提醒放 .env + conftest 隔离 test_defaults
飞书 webhook(敏感)和 ENABLED 不该硬编码进 config.py 默认值(会泄露进仓库/误带到生产默认开),
统一放 .env(gitignore)。conftest 强制 COMPARE_ALERT_ENABLED=false, test_defaults 不受 .env 干扰。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-05 10:20:49 +08:00
guke af229e2a7b feat(compare-alert): 飞书消息改行式富文本(明细含手机号/版本/原因/trace超链接)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 10:12:49 +08:00
guke 576b94b4bb fix(compare-alert): 飞书时间标签用北京时区(CN_TZ) + main.py import 排序
final review 发现 window_label 用 datetime.now() 本地时钟,UTC 服务器上飞书标题时间差 8h;改用 CN_TZ。
顺带 ruff --fix 修 main.py 挂载 worker 时引入的 import 排序(isort)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 19:56:48 +08:00
guke 46247fb3a9 feat(compare-alert): lifespan 挂载 compare-alert worker
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-04 19:38:11 +08:00
guke d0169ffb54 feat(compare-alert): 扫描 worker(水位/冷启动/发送失败不推进)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-04 19:34:02 +08:00
guke bc321c1c64 feat(compare-alert): 飞书群机器人 notifier(关键词验证,无签名)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-04 19:30:17 +08:00
guke 7891984cd1 feat(compare-alert): 飞书汇总消息格式化(分组/两级截断/关键词)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-04 19:28:11 +08:00
guke 20cbc9e35e feat(compare-alert): 记录级报警规则纯函数(T1/T2/T5/T6)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-04 19:25:44 +08:00
guke 02d6300442 feat(compare-alert): 加 COMPARE_ALERT_* 配置项与关键词解析
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-04 19:22:44 +08:00
guke 46ffa41931 feat(compare-alert): comparison_record 加 updated_at 列+索引+回填(报警水位)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-04 19:20:23 +08:00
guke 03129e059f feat(admin): 比价记录改「技术成功/失败」口径,外部缺失记为成功 (#217)
背景
admin 比价记录页 / 概览 / 大盘此前把「未找到店、未找到菜、门店打烊、单点不配送、平台不支持、未满起送」统统显示/统计为失败——根因是 #209 把这些业务结局归一化成记录级 status='failed' 落库。但它们其实是比价流程正常跑完、只是外部原因导致结果缺失;与「系统技术故障」混为一谈后,管理员排查时无法区分「是我们系统的锅」还是「目标平台本来就没这家店/这些菜」。

方案
admin 后台改用技术完成率口径:流程跑完(非 running)且非纯技术故障 → 记为成功,有外部缺失的前端标绿「成功」+ ⚠(hover 看具体原因);只有真正的技术故障 failed 才是失败。

口径收敛到新模块 app/admin/repositories/comparison_outcome.py,被列表下发 / 概览 / 大盘 / 状态筛选共同消费(单一真相源)。
原始业务结局取自 raw_payload.record_status(3 级 coalesce 兜底,兼容历史残留;Python 派生与 SQL 判定 bit 一致)。
仅 admin,不碰 C 端 / #209 落库 / 奖励逻辑——admin 关心「系统跑没跑成」,C 端关心「省没省到钱」,刻意分层。

口径映射
原始 record_status | admin 状态 | hover 提示
-- | -- | --
success | 🟢 成功 | —
below_minimum | 🟢 成功 ⚠ | 未满起送
store_closed | 🟢 成功 ⚠ | 门店打烊
store_not_found | 🟢 成功 ⚠ | 未找到店
items_not_found | 🟢 成功 ⚠ | 未找到菜
no_delivery | 🟢 成功 ⚠ | 单点不配送
unsupported | 🟢 成功 ⚠ | 平台·场景不支持
failed(纯技术故障) | 🔴 失败 | —
cancelled / running |  中途退出 / 🔵 进行中 | —

改动清单
新增 comparison_outcome.py:derive_admin_outcome(列表 Python 派生) + admin_success_sql(聚合/筛选 SQL 判定)。
列表/详情 下发 admin_status + outcome_hint(瞬态挂载,零额外查询)。
概览 comparison_records_summary:success / completed / 耗时分位改 admin 口径。
大盘 dashboard_overview:比价成功率改 admin 口径(顺带补齐 #209 未同步大盘的 below_minimum)。
状态筛选 _comparison_status_condition:筛「成功」含 6 类、筛「失败」仅纯技术故障;清理 #209 遗留死常量。
文档 补 admin 口径说明。

---------

Co-authored-by: guke <guke@autohome.com.cn>
Reviewed-on: #217
2026-08-04 18:49:30 +08:00
guke bc2ed5de56 feat(compare): 新增只读当日比价额度查询 GET /compare/quota (#216)
## 概述
新增只读接口 `GET /api/v1/compare/quota`,供客户端「跳外卖 App」型比价入口在点击时**前置查询当日比价是否已达上限**(100 次/日),超限就地提示、不进入比价流程。

配套客户端 PR:比价/领券异常提示统一 + 4 入口上限拦截(shaguabijia-app-android 同名分支)。

## 改动
- `app/repositories/comparison.py`:新增只读 `get_daily_compare_used(db, user_id, reset_at)` —— 按 user_id + 北京时间自然日 COUNT,**窗口计算逐行复刻写路径 `reserve_daily_start`**,保证前置查询与真发起的 429 gate 口径不漂移。
- `app/schemas/compare_record.py`:新增 `CompareQuotaOut(exhausted, used, limit)`。
- `app/api/v1/compare_record.py`:新增 `GET /quota` 端点,硬鉴权 `CurrentUser`、只读不预占;`limit_policy.resolve` 传 `phone + device_id`(与 `/compare/start` 一致,命中 device 白名单)。

## 测试
- `pytest tests/test_compare_daily_limit.py`:**8 passed**(4 既有 + 4 新增,含 device 白名单 parity 测试,锁定 `/quota` 与 `/start` 口径一致)。

## 合并 / 部署注意 ⚠️
- 本 PR 应**先于客户端 PR 合并 + 部署**(客户端点击前置拦截依赖此接口;未部署时客户端 fail-open 放行)。
- 只读、无副作用、不改写路径逻辑,风险低。

---------

Co-authored-by: guke <guke@autohome.com.cn>
Reviewed-on: #216
2026-08-04 16:38:46 +08:00
guke 5a66c302cb fix(admin): 修 queries import 排序 + 补比价详情 admin 字段断言
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 16:01:24 +08:00
guke 09b9381d03 feat(admin): 比价记录列表/详情下发 admin_status + outcome_hint
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 15:48:55 +08:00
guke 7419f35f4b fix(admin): 口径模块 SQL 侧 nullif 对齐空串,消除 Python/SQL 分歧
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 15:44:58 +08:00
guke 9de73152ec feat(admin): 比价记录展示口径共享模块(外部缺失判为成功)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 15:35:32 +08:00
guke 9036bc5a08 fix: admin 比价记录列表兼容 user_id 为空的孤儿行
软鉴权/匿名下 pricebot 帧0 建行时 user_id 可空(见 models.comparison),admin
全看含孤儿行;但 AdminComparisonListItem.user_id 声明为必填 int,Pydantic v2
对 None 抛 ValidationError,致 GET /admin/api/comparison-records 列表接口 500。
改为 int|None(详情接口继承一并修复),并补回归测试
test_comparison_records_list_tolerates_orphan_null_user。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 10:49:57 +08:00
linkeyu 1a61cb5a65 修复 DeepSeek V4 Flash TOKEN 成本高估 (#214)
## 改动
- 为 deepseek-v4-flash 配置 DashScope 华北 2 官方单价:输入 ¥1 / 输出 ¥2(每百万 Token)
- 定向重算历史上误用 3/15 兜底价冻结的成本和价格快照
- 保留已有人工单价及配置生效时间,避免影响历史缺失成本回填
- 增加迁移升级、降级和原配置保留测试

## 验证
- 相关测试:17 passed
- ruff check:通过

---------

Co-authored-by: guke <guke@wonderable.ai>
Co-authored-by: linkeyu <798648091@qq.com>
Co-authored-by: unknown <798648091@qq.com>
Reviewed-on: #214
Co-authored-by: linkeyu <linkeyu@wonderable.ai>
Co-committed-by: linkeyu <linkeyu@wonderable.ai>
2026-08-01 23:25:27 +08:00
linkeyu 67ac2dcbbb fix: 领券成功率剔除中途退出 (#213)
## 改动说明
- 领券成功率分母改为:发起数 - 中途退出数
- failed 与 started 仍保留在分母
- 接口新增 abandoned 和 success_denominator 字段
- 中途退出已有单券结果时返回真实成功/尝试数
- 中途退出且没有逐券终态时明确返回 0/0
- 新增 point_event_count,区分「只有 skipped、无有效计分结果」和「完全无逐券事件」
- 用户领券记录接口同步聚合逐券结果

## 验证
- 相关后端测试:21 passed
- Ruff:通过
- 线上数据库只读核对:空白记录确实没有 coupon_claim_event

配套前端:WonderableAI/shaguabijia-admin-web#101

---------

Co-authored-by: guke <guke@wonderable.ai>
Co-authored-by: linkeyu <798648091@qq.com>
Reviewed-on: #213
Co-authored-by: linkeyu <linkeyu@wonderable.ai>
Co-committed-by: linkeyu <linkeyu@wonderable.ai>
2026-08-01 23:24:54 +08:00
zuochenyong 08a49504fa fix(反馈): 优化图片处理与静态资源缓存 (#211)
Co-authored-by: guke <guke@wonderable.ai>
Co-authored-by: exinglang <exinglang@qq.com>
Reviewed-on: #211
Co-authored-by: zuochenyong <zuochenyong@wonderable.ai>
Co-committed-by: zuochenyong <zuochenyong@wonderable.ai>
2026-08-01 23:24:17 +08:00
linkeyu ab2de6ec79 修复:统一比价记录状态枚举口径 (#209)
## 变更说明

- below_minimum 归入成功,保留原始业务结局
- store_closed/store_not_found/items_not_found/no_delivery/unsupported 归入失败
- running 保持进行中生命周期状态
- 历史细分状态迁移为三态终态,迁移可逆
- 后台成功/失败筛选及汇总兼容迁移前历史值

## 验证

- 相关回归:48 passed
- Ruff:通过
- Alembic:单一 head

---------

Co-authored-by: guke <guke@wonderable.ai>
Co-authored-by: linkeyu <798648091@qq.com>
Co-authored-by: unknown <798648091@qq.com>
Reviewed-on: #209
Co-authored-by: linkeyu <linkeyu@wonderable.ai>
Co-committed-by: linkeyu <linkeyu@wonderable.ai>
2026-08-01 23:09:21 +08:00
linkeyu 84251770b4 修复:首页轮播排除测试账号数据 (#215)
## 背景

线上首页轮播候选记录中,测试账号 u33(11111111111)贡献约 51.8% 的真实记录,导致该用户频繁出现。

## 改动

- App 首页轮播真实记录查询排除全部已配置测试账号
- 后台首页轮播可展示记录同步采用相同过滤口径
- 同时兼容 TEST_ACCOUNT_PHONE 与 TEST_ACCOUNT_PHONES
- 测试账号配置变化时立即使轮播查询缓存失效
- 不删除历史业务数据,仅在展示查询中排除

## 验证

- ruff check:通过
- pytest tests/test_ops_marquee.py -q:1 passed

---------

Co-authored-by: linkeyu <798648091@qq.com>
Reviewed-on: #215
Co-authored-by: linkeyu <linkeyu@wonderable.ai>
Co-committed-by: linkeyu <linkeyu@wonderable.ai>
2026-08-01 21:35:06 +08:00
marco 2e91c9f72f ecpm保底是1 2026-08-01 02:57:45 +08:00
marco a69b7d777d feat(compare/coupon): trace_id 统一由后端签发,前端不再本地生成 (#212)
Reviewed-on: #212
2026-07-31 23:52:19 +08:00
marco 0fc8521c3b feat(compare/coupon): trace_id 统一由后端签发,前端不再本地生成 (#210)
一次比价/领券的 trace_id 改由后端签发,让前端 SLS 运行日志(trace_id 索引列)、
app-server 比价记录与领券流水、pricebot trace 目录/run.log/trace_url 全链共用同一个
id 查到底(此前前端各业务自己 randomUUID,虽同链但非后端签发、也无单一签发点)。

- POST /api/v1/compare/start(预占额度,任务第一个请求,签发与建 running 行合一):
  请求 trace_id 改可选,缺省时服务端签发 uuid;响应新增 trace_id 字段返回(签发的或
  回显客户端带来的)。客户端带值则沿用——老客户端兼容 + 同 trace 重试幂等。
- POST /api/v1/coupon/session:started 帧缺 trace_id 时签发并随响应返回(签发不依赖
  写库成功);非 started 帧缺 trace_id 不签发、不写库(收尾没有 id 只能是异常调用,
  签发新 id 只会造出查不到发起信息的孤儿行)。新增 CouponSessionOut 响应模型——原
  dict[str,bool] 注解无法承载字符串 trace_id,FastAPI 响应校验会炸。
- 测试:更新 2 处旧断言(响应体多出 trace_id 字段),新增 compare 不带 id 签发用例 +
  coupon started 签发/回显、终尾缺 id 跳过写库 3 个用例。全量 30 passed + ruff clean。

配合 shaguabijia-app-android 同名分支 feat-unify-trace-id-backend-issued 的前端换源改动。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Reviewed-on: #210
2026-07-31 23:11:35 +08:00
linkeyu 15fb73791f 功能:统一限制策略与白名单管理 (#207)
## 需求背景
将比价、短信与登录、广告、引导与账号、风控免告警等限制统一配置,并支持按手机号或设备设置有有效期的临时白名单。

## 主要改动
- 新增统一限制策略注册表、全局 JSON 配置与白名单覆盖表
- 新增白名单管理、设备检索、批量追加与主体统一编辑接口
- 接入比价、短信登录、广告奖励、引导视频、账号换绑及风险告警调用链
- 保留旧配置接口兼容,并同步统一策略全局值
- 增加单主体唯一有效期、恢复全局、审计日志和风险事件自动处理
- 增加数据库迁移及完整回归测试

## 验证
- 白名单、权限、配置及风控测试 50 项通过
- 短信、登录、比价、广告关联测试 98 项通过
- Ruff 与 Python 编译检查通过
- Alembic 保持单一 head
- 已同步最新 main

---------

Co-authored-by: unknown <798648091@qq.com>
Reviewed-on: #207
Co-authored-by: linkeyu <linkeyu@wonderable.ai>
Co-committed-by: linkeyu <linkeyu@wonderable.ai>
2026-07-31 17:08:06 +08:00
zuochenyong d9177cbb71 fix(推送): 隔离推送设备账号绑定并支持定向联调,新建22222222222测试账号 (#208)
Co-authored-by: exinglang <exinglang@qq.com>
Reviewed-on: #208
Co-authored-by: zuochenyong <zuochenyong@wonderable.ai>
Co-committed-by: zuochenyong <zuochenyong@wonderable.ai>
2026-07-31 14:34:07 +08:00
guke eeecb5faf0 fix(广告发奖): eCPM 过低单份金币四舍五入成 0 时兜底发 1,避免看了广告被记 too_short 零发 (#205)
## 背景 / 问题

线上 record 4667:用户在比价等候期看满一条低 eCPM(¥0.43 CPM)的信息流广告(≥10s),
但单份金币按公式 `0.43/1000 × 0.1(档位因子) × 1.0(LT因子) × 10000 = 0.43`,四舍五入成 **0**。
在 `grant_feed_reward` 里 `unit_cap=0` → `coin<=0` → 记 `too_short` 零发。

结果:**用户看满了广告却什么都没拿到**,还被标成"时长不足"。

## 改动

`app/core/rewards.py` 的 `calculate_ad_reward_coin`(发奖与后台审计对账的**唯一口径**):

- 有真实正 eCPM 时,单份金币 floor 到 1(`max(0, …)` → `max(1, …)`)。
- eCPM 缺失 / 为 0 / 非法(没有真实广告价值)时提前 `return 0` —— 不凭空铸币、不破坏 `ecpm_missing` 语义。
关键设计
if ecpm_yuan <= 0: return 0 是防铸币防线:没它的话 reward_video 路径 ecpm="0" 会被 floor 成 1。
不影响防刷:上限仍由 AD_ECPM_MAX_FEN(¥500 CPM)钳顶;LT 因子最低 1.0、从不归零,限流靠每日 500 次上限。
正常量级 eCPM 本就 ≥1,下限不改变其取值(纯回归保护)。

---------

Co-authored-by: guke <guke@autohome.com.cn>
Reviewed-on: #205
2026-07-31 11:18:00 +08:00
guke 31bff63ed4 feat(sms): 短信验证码 极光→创蓝 容灾 fallback (#204)
背景
短信验证码已是可切换 provider 架构(极光 / 创蓝)。极光(默认)一旦供应商侧故障(欠费 / 网络 / 服务异常),/sms/send 直接 503 → 用户收不到码、登录中断。本 PR 把极光设为主、创蓝设为备,在极光供应商不可用时自动转创蓝补发,并让后台可区分每次实际走的渠道。

方案(4 个关键决策)
# | 决策 | 结论
-- | -- | --
A | fallback 触发范围 | 仅主返回「供应商不可用」(SmsError.status_code == 503:网络 / 余额 / 服务故障)才转备。本地冷却 & 超频(429)、手机号无效(400)不转——不绕过防刷、不为无效号白烧
B | 校验路由 | try-both:极光转创蓝后码在创蓝内存,校验遍历「启用的 fallback 链」(主→备),任一命中即通过;关闭 fallback 时链中只有极光、创蓝零参与
C | 后台可见性 | 成功侧 EVENT_SMS_SEND.details 记 provider / fallback + 分派层日志,风控后台可按号/设备查本次走哪家、是否 fallback
D | 默认开关 | SMS_FALLBACK_PROVIDER 默认空=关(保持现状零风险),生产设 chuanglan 开启,置空即秒回退。仅 Mode B(jiguang/chuanglan)互为主备

---------

Co-authored-by: guke <guke@autohome.com.cn>
Reviewed-on: #204
2026-07-31 11:17:30 +08:00
guke ef0ab9d95a fix(短信): 号码无效提示由「手机号无效」改为「请输入有效的手机号」 (#206)
极光/阿里云/创蓝三处 provider 的号码无效错误统一改成更友好的用户提示。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: guke <guke@autohome.com.cn>
Reviewed-on: #206
2026-07-30 19:15:45 +08:00
zuochenyong 06cd718610 修复之前错误配置的端口 (#203)
Co-authored-by: exinglang <exinglang@qq.com>
Reviewed-on: #203
Co-authored-by: zuochenyong <zuochenyong@wonderable.ai>
Co-committed-by: zuochenyong <zuochenyong@wonderable.ai>
2026-07-30 16:32:32 +08:00
zuochenyong aa1a1240f2 fix(push): 完善厂商推送排障日志 (#202)
Co-authored-by: exinglang <exinglang@qq.com>
Reviewed-on: #202
Co-authored-by: zuochenyong <zuochenyong@wonderable.ai>
Co-committed-by: zuochenyong <zuochenyong@wonderable.ai>
2026-07-30 14:05:05 +08:00
guke b6ddb275f4 fix(comparison): 比价 best 无 is_best 时兜底回落「含源最低价」 (#201)
背景
_derive_from_platforms 是比价记录的单一真相源派生函数(pricebot done 帧带 platforms 时走它),正常路径直接取 platforms 里 is_best=true 的那行当最低价。

线上出现一类帧(如记录 id 3304):全平台 has_dish_diff(菜品「相似替换 / 价格仅供参考」),pricebot 认为无法认定权威最低价,于是一个 is_best 都不标。此时旧逻辑 best=None,导致 best_platform_id / best_price_cents / saved_amount_cents / is_source_best 整条落 NULL,连锁反应:

首页比价价显示 0.00
记录页无最低价红框
省额丢失、「累计发现可省」漏计
方案
无 is_best 时兜底:在有价行里取最低价当参考 best。

关键设计点 —— 候选池含源(而非仅目标):源平台常年全菜、价可信。若源本身最便宜(其余都是更贵的相似替换),则 best 回落到源、saved=0、is_source_best=True。这与仓库老派生函数 _derive 的既有语义(「全目标缺菜 → 回落源、不虚报省」)完全一致,三个派生函数行为对齐。

⚠️ 若像分支首个提交那样排除源、强选最低目标,当源最便宜时会选中更贵目标 → saved 变负,倒扣 get_stats 的「累计发现可省」(该聚合按 status='success' 求和、不带 >0 过滤)。第二个提交据此修正为含源。

影响面 / 兼容性
只影响「带 platforms 且无任何 is_best」这一条兜底分支;正常有 is_best 的路径不变。
老客户端不带 platforms → 走 _derive,不受影响。
纯派生逻辑,无 schema / 无迁移,回滚成本低。

---------

Co-authored-by: guke <guke@autohome.com.cn>
Reviewed-on: #201
2026-07-30 11:00:35 +08:00