Files
cursor/plans/IMEI FB-3a02c714.plan.md
ray zhou 2dd9f17da9 ok
2026-06-29 14:51:55 +08:00

177 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- 3a02c714-3517-4fd8-ba3a-0c9172c34575 -->
---
todos:
- id: "parse-fbc-fbclid"
content: "在 insertADTag 回退路径实现 fbc→fbclid 解析与字段映射ad_fbp/ad_ua/ad_ip"
status: pending
- id: "redis-pop-latest"
content: "实现 resolveLatestDotPayloadBySourcezPopMax user:dot:set:{source} + unserialize 校验"
status: pending
- id: "imei-only-guard"
content: "回退逻辑仅 IMEI 新注册触发(覆写 ImeiRegisterService::insertADTag 或在 Abstract 中加平台/类型判断)"
status: pending
- id: "redis-cleanup"
content: "可选SiteController 或 pop 侧增加 ZSet 过期/长度清理"
status: pending
- id: "verify-fbdot"
content: "联调:落地页上报 → IMEI 注册 → 检查 user tag 与 FbDot 注册打点"
status: pending
isProject: false
---
# IMEI 注册 FB 像素归因补写方案评估
## 结论
**方案可行**,且与现有设计意图一致:[`slot_user/app/api/controller/SiteController.php`](slot_user/app/api/controller/SiteController.php) 负责落地页采集,[`slot_user/app/api/controller/UserController.php`](slot_user/app/api/controller/UserController.php) 里已有被注释的 `zPopMax` 回退逻辑,[`slot_user/app/service/register/ImeiRegisterService.php`](slot_user/app/service/register/ImeiRegisterService.php) 在新用户路径调用 `insertADTag`
你已确认范围:**仅 IMEI 新用户注册**(现有 `insertADTag` 调用点),不覆盖老用户登录补写。
---
## 现有数据流
```mermaid
sequenceDiagram
participant Landing as LandingPage_FB_IAB
participant Site as SiteController_fb
participant Redis as Redis_ZSet
participant App as iOS_App
participant Reg as ImeiRegisterService
participant Tag as insertADTag
participant Dot as slot_console_FbDot
Landing->>Site: POST fbp,fbc,ua,source
Site->>Redis: zAdd user:dot:set:{source}
App->>Reg: IMEI register (无 fb 参数)
Reg->>Tag: insertADTag(uid)
Tag->>Redis: 若无 fbclid 则取最新
Tag->>Tag: UserInfoCache 写入 ad_fbc/ad_fbp/ad_ua/ad_ip
Dot->>Tag: 读 ad_fbc 作为 fbclid 打 CompleteRegistration
```
| 环节 | 现状 |
|------|------|
| 落地页采集 | [`SiteController::fb`](slot_user/app/api/controller/SiteController.php) 校验 `FB_IAB` UA写入 `user:dot:set:{source}`member 为 `serialize(fbp,fbc,ua,ip,source)`score 为 `microtime` |
| IMEI 注册 | [`ImeiRegisterService::run()`](slot_user/app/service/register/ImeiRegisterService.php) 新用户分支 L115 调用 `insertADTag` |
| 写入用户标签 | [`insertADTag`](slot_user/app/service/register/AbstractRegisterService.php) 当前固定用 `$this->fbclid``ad_fbc`iOS 客户端通常为空 |
| FB 正式打点 | [`FbDot`](slot_console/app/command/dot/FbDot.php) 读 `ad_fbc`[`FbService::generateFBC()`](slot_console/app/service/dot/FbService.php) 用 fbclid 生成 fbc/fbp |
---
## 方案为何可行
1. **基础设施已就绪**Redis key [`USER_DOT_SET_KEY`](slot_user/app/service/RedisKeyManagerService.php)、落地页写入、注册侧回退思路UserController 注释代码)均已存在。
2. **iOS 场景匹配**iOS 包拿不到 WebView cookie但 FB 广告落地页在 `FB_IAB` 内可拿到 `fbp`/`fbc`,先上报再装 App 是合理补偿路径。
3. **与 PWA 不冲突**PWA 仍优先用请求里的 `fbclid`;仅当 `$this->fbclid`(及关联 fb 字段)为空时才回退 Redis。
4. **打点链路可通**`FbDot` 只要求 `ad_fbc` 非空;存入正确 fbclid 后现有 `FbService` 可继续生成 CAPI 所需 fbc/fbp。
---
## 必须处理的 4 个技术点
### 1. 字段映射(最关键)
当前 `insertADTag` 写:
```348:352:slot_user/app/service/register/AbstractRegisterService.php
protected function insertADTag(int $uid)
{
// ...
$userInfo->setInfo(['ad_fbc' => $this->fbclid, 'ad_fbp' => $this->fbp, ...], '');
}
```
Redis 里存的是落地页 **`fbc`/`fbp`**,不是 `fbclid`。而 `FbService` 期望 `ad_fbc` 存 **原始 fbclid**(见测试数据与 `generateFBC()` 实现)。
**实现要求**:从 Redis 取出 `fbc` 后,需解析出 fbclid
- `fbc` 格式:`fb.{subdomainIndex}.{creationTime}.{fbclid}`
- 取最后一个 `.` 之后的片段作为 fbclid 写入 `ad_fbc`
- `ad_fbp`、`ad_ua`、`ad_ip` 直接使用 Redis 中的值(比服务端随机生成 fbp 更准确)
- 若 `fbc` 为空SiteController 允许),则无法补 attribution应跳过写入并打 warn 日志
### 2. 取「最新」的方式
- 使用 `ZPOPMAX user:dot:set:{source} 1`(与注释代码一致),**消费**一条,避免同一条数据被下一个用户重复使用。
- 若 `insertADTag` 后续 `setInfo` 失败,可考虑不 pop 或失败写回(当前 `insertADTag` 有 try/catch失败只记日志pop 后丢失风险可接受,或改为「先 ZREVRANGE 再 ZREM 成功后再写」)。
### 3. 同渠道并发碰撞(已知局限)
按 `source` 取全局最新一条,**无法 100% 保证**「这条 fbc 就是当前这台设备」。同渠道短时间多用户注册可能串 attribution。
可接受的 MVP与当初 UserController 注释方案一致。若要提升准确率,后续可加:
- 时间窗口(如 30 分钟内)
- IP 辅助匹配(落地页 IP vs 注册 IP不完全可靠
- 落地页生成 `dot_token` 透传到 App需客户端配合改动更大
### 4. Redis 集合维护
当前 `zAdd` **无 TTL、无上限**,长期会膨胀。建议实现时顺带:
- 写入时 `ZREMRANGEBYSCORE` 清理超过 N 天(如 7 天)的旧 member
- 或按 source 限制 ZSet 最大长度
---
## 推荐改动位置(实现阶段)
不在 `UserController::register` 做(你已选 register_only + insertADTag 内聚),建议:
**主改文件**[`slot_user/app/service/register/AbstractRegisterService.php`](slot_user/app/service/register/AbstractRegisterService.php)
- 新增私有方法,例如 `resolveLatestDotPayloadBySource(string $source): ?array`
- `Redis::zPopMax(RedisKeyManagerService::getDotSetKey($source), 1)`
- `unserialize` + 校验必需字段(至少 `fbp` 非空;`fbc` 非空才能解析 fbclid
- 改造 `insertADTag`
- 若 `$this->fbclid` 为空,调用上述方法补全 `fbclid/fbp/ua/ip`
- 若补全后仍无 fbclid`return`(不写空 tag
- 否则按现有逻辑 `setInfo`
**可选**:若希望仅 IMEI 走回退、不影响 Name 注册,可把回退逻辑放在 [`ImeiRegisterService`](slot_user/app/service/register/ImeiRegisterService.php) 覆写 `insertADTag`,而非改 Abstract 基类。
---
## 前置条件(产品/客户端)
方案生效依赖用户路径:
1. 用户从 FB 广告进入落地页UA 含 `FB_IAB`
2. 落地页成功调用 `SiteController::fb`(带正确 `source`
3. 用户在合理时间窗口内完成 iOS 首次 IMEI 注册,且 `source` 与落地页一致
若用户跳过落地页直接装包注册,仍无法归因——这是架构固有限制,不是实现 bug。
---
## 风险清单
| 风险 | 级别 | 说明 |
|------|------|------|
| fbc 为空 | 中 | 落地页允许空 fbc补写无效FbDot 仍会报 fbclid 为空 |
| 同 source 串号 | 中 | 多用户并发时可能拿错上一条 fbc |
| pop 后写失败 | 低 | 数据丢失,仅影响单次 attribution |
| ad_fbp 未被 FbDot 直接使用 | 低 | 当前 FbDot 用 fbclid 重新 generate fbp落地页 fbp 写入 user tag 但 CAPI 仍用生成值。MVP 可接受;若要最佳 attribution 需后续改 FbDot 优先用 `ad_fbp`/真实 `fbc` |
---
## 验证建议
1. 落地页 mockPOST `SiteController::fb`,确认 Redis `user:dot:set:{source}` 有 member
2. IMEI 新用户注册(不带 fb 参数),查 `UserInfoCache` / user tag 中 `ad_fbc`、`ad_fbp`、`ad_ua`
3. 触发 `slot_console` 注册打点,确认 `FbDot::registerEvent` 不再报 fbclid 为空
4. 并发两条落地页 + 两次注册,观察是否出现串号(评估是否需加时间窗)
---
## 总结
**可行,且是 iOS 上架场景下合理的补偿方案。** 实现重点不是「能不能取 Redis」而是
1. 正确从 `fbc` 解析 fbclid 再写入 `ad_fbc`
2. 仅在客户端无 fb 参数时回退
3. 用 `zPopMax` 消费最新一条
4. 接受同渠道并发下的 attribution 碰撞,或后续迭代匹配策略
按你确认的范围,**只改 IMEI 新注册 `insertADTag` 即可**,无需动登录链路。