This commit is contained in:
ray zhou
2026-06-29 14:51:55 +08:00
parent 225fb2bd28
commit 2dd9f17da9
319 changed files with 29461 additions and 9412 deletions

View File

@@ -0,0 +1,176 @@
<!-- 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` 即可**,无需动登录链路。