docs: WaHappy 开发文档
孤儿分支,只含 docs/。内容包括: - 项目总览 / 主脚本编写指南 / 日志指南 - 上传成绩开发文档 / UpsertUserAll 踩坑清单(28 条) - 真机 Hook 对照报告 / 实跑验证记录 - 购买功能票流程(含被服务端拒绝的排查过程) · 扫码登录协议技术规范
This commit is contained in:
@@ -0,0 +1,487 @@
|
||||
# UpsertUserAll 实跑数据 vs 真机日志 逐条对比
|
||||
|
||||
> **本文档数据来源**
|
||||
> - **实跑侧**:用你提供的二维码真实跑通了「扫码 → 预览 → 登录 → 抓快照 → 构造 UpsertUserAll → 登出」,
|
||||
> 报文由 `api/upsert_user_all.py` 生成(**dry-run,未发包**)
|
||||
> - **真机侧**:`source/2026-10-05.log:123` 的 UpsertUserAllApi 请求报文
|
||||
>
|
||||
> **结论速览**
|
||||
>
|
||||
> | 维度 | 结果 |
|
||||
> |---|---|
|
||||
> | 顶层字段 | 7/7 结构一致 |
|
||||
> | 36 个节点 | **形态 36/36 全部一致** |
|
||||
> | 11 条位串 | **11/11 全部一致**(含 `isNewFavoriteList="22212"`) |
|
||||
> | userData 字段 | 76/82 命中(6 处差异全部可解释,见 §5) |
|
||||
> | userPlaylog 字段 | 106/110 命中(4 处差异全部可解释,见 §6) |
|
||||
|
||||
---
|
||||
|
||||
## 一、本次实跑记录
|
||||
|
||||
| 项目 | 值 |
|
||||
|---|---|
|
||||
| 二维码时间戳 | `261005175231`(2026-10-05 17:52:31) |
|
||||
| user_id | 12345678 |
|
||||
| 昵称 / Rating | ☆Astral☆ / 15000 |
|
||||
| loginId | **7693120247791879616** |
|
||||
| loginDateTime | **1791222938** |
|
||||
| dateTime(Auth.AuthTime) | **1791194138** |
|
||||
| 快照节点 | 19 个全部成功(`_failed: []`) |
|
||||
| 是否发包 | **否**(dry_run=True) |
|
||||
|
||||
### 快照抓到的条数(对比真机日志的 Get 侧)
|
||||
|
||||
| 节点 | 本次实跑 | 真机 2026-10-05 | 说明 |
|
||||
|---|---:|---:|---|
|
||||
| userCharacterList | 312 | 312 | 一致 |
|
||||
| userMapList | 104 | 104 | 一致 |
|
||||
| userLoginBonusList | 38 | 38 | 一致 |
|
||||
| userItemList | 1070 | 1070 | 一致 |
|
||||
| userMusicDetailList | **1037** | 739(组数) | 实跑已摊平成单条,真机是 739 组嵌套 |
|
||||
| userCourseList | 16 | 16 | 一致 |
|
||||
| userChargeList | 3 | 3 | 一致 |
|
||||
| userFavoriteList | 5 | 5 | 一致 |
|
||||
| userMissionDataList | 6 | 6 | 一致 |
|
||||
| userIntimateList | 2 | 2 | 一致 |
|
||||
| userGhost | 15 | 15 | 一致 |
|
||||
|
||||
> ★ `userMusicDetailList` 1037 是**摊平后**的 (musicId, level) 条目数;真机日志的 739 是**按曲分组**的组数。
|
||||
> 两者不是同一口径,摊平后 739 组共 1037 条。
|
||||
|
||||
---
|
||||
|
||||
## 二、顶层结构对比
|
||||
|
||||
```jsonc
|
||||
// 实跑产出(api/upsert_user_all.py 生成)
|
||||
{
|
||||
"userId": 12345678,
|
||||
"playlogId": 7693120247791879616, // = UserLoginApi 的 loginId
|
||||
"isEventMode": false,
|
||||
"isFreePlay": false,
|
||||
"loginDateTime": 1791222938, // = UserLoginApi 的 loginDateTime
|
||||
"userPlaylogList": [ { ...110 字段... } ],
|
||||
"upsertUserAll": { ...36 节点... }
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 真机 | 本次实跑 | 判定 |
|
||||
|---|---|---|---|
|
||||
| `userId` | 12345678 | 12345678 | 一致 |
|
||||
| `playlogId` | 7692848101484135861 | 7693120247791879616 | **不同**(不同次登录,正常) |
|
||||
| `isEventMode` | false | false | 一致 |
|
||||
| `isFreePlay` | false | false | 一致 |
|
||||
| `loginDateTime` | 1791159576 | 1791222938 | **不同**(不同次登录,正常) |
|
||||
| `userPlaylogList` | 1 条 | 1 条 | 一致 |
|
||||
| `upsertUserAll` | 36 节点 | 36 节点 | 一致 |
|
||||
|
||||
> `playlogId` / `loginDateTime` 两次必然不同 —— 它们是**本次登录会话**产生的,
|
||||
> 不是固定值。关键是它们与**本次** `UserLoginApi` 的返回值一致(已核对)。
|
||||
|
||||
---
|
||||
|
||||
## 三、36 个节点逐节点对比
|
||||
|
||||
| # | 节点 | 真机 | 实跑 | 形态 |
|
||||
|---|---|---|---:|---|
|
||||
| 1 | `userData` | 1 条 | 1 条 | 一致 |
|
||||
| 2 | `userExtend` | 1 条 | 1 条 | 一致 |
|
||||
| 3 | `userOption` | 1 条 | 1 条 | 一致 |
|
||||
| 4 | `userCharacterList` | 5 条 | 5 条 | 一致 |
|
||||
| 5 | `userGhost` | 0 条 | 0 条 | 一致 |
|
||||
| 6 | `userMapList` | 1 条 | 1 条 | 一致 |
|
||||
| 7 | `userLoginBonusList` | 0 条 | 0 条 | 一致 |
|
||||
| 8 | `userRatingList` | 1 条 | 1 条 | 一致 |
|
||||
| 9 | `userItemList` | 0 条 | 0 条 | 一致 |
|
||||
| 10 | `userMusicDetailList` | 1 条 | 1 条 | 一致 |
|
||||
| 11 | `userCourseList` | 0 条 | 0 条 | 一致 |
|
||||
| 12 | `userFriendSeasonRankingList` | 0 条 | 0 条 | 一致 |
|
||||
| 13 | `userChargeList` | **3 条** | **3 条** | 一致(整表回传) |
|
||||
| 14 | `userFavoriteList` | **5 条** | **5 条** | 一致(恒 5 条) |
|
||||
| 15 | `userActivityList` | 1 条 | 1 条 | 一致 |
|
||||
| 16 | `userMissionDataList` | 6 条 | 6 条 | 一致 |
|
||||
| 17 | `userWeeklyData` | 对象 | 对象 | 一致 |
|
||||
| 18 | `userGamePlaylogList` | 1 条 | 1 条 | 一致 |
|
||||
| 19 | `user2pPlaylog` | 对象 | 对象 | 一致 |
|
||||
| 20 | `userIntimateList` | 0 条 | 0 条 | 一致 |
|
||||
| 21 | `userShopItemStockList` | 0 条 | 0 条 | 一致 |
|
||||
| 22 | `userGetPointList` | 0 条 | 0 条 | 一致 |
|
||||
| 23 | `userTradeItemList` | 0 条 | 0 条 | 一致 |
|
||||
| 24 | `userFavoritemusicList` | 0 条 | 0 条 | 一致 |
|
||||
| 25 | `userKaleidxScopeList` | 0 条 | 0 条 | 一致 |
|
||||
| 26-36 | 11 条 `isNew*` 位串 | — | — | 见下节 |
|
||||
|
||||
**形态命中 36/36。**
|
||||
|
||||
---
|
||||
|
||||
## 四、11 条位串对比(全部一致)
|
||||
|
||||
| 位串 | 真机 | 实跑 | 一致 |
|
||||
|---|---|---|---|
|
||||
| `isNewCharacterList` | `"00000"` | `"00000"` | ✅ |
|
||||
| `isNewMapList` | `"0"` | `"0"` | ✅ |
|
||||
| `isNewLoginBonusList` | `""` | `""` | ✅ |
|
||||
| `isNewItemList` | `""` | `""` | ✅ |
|
||||
| `isNewMusicDetailList` | `"0"` | `"0"` | ✅ |
|
||||
| `isNewCourseList` | `""` | `""` | ✅ |
|
||||
| **`isNewFavoriteList`** | **`"22212"`** | **`"22212"`** | ✅ |
|
||||
| `isNewFriendSeasonRankingList` | `""` | `""` | ✅ |
|
||||
| `isNewUserIntimateList` | `""` | `""` | ✅ |
|
||||
| `isNewFavoritemusicList` | `""` | `""` | ✅ |
|
||||
| `isNewKaleidxScopeList` | `""` | `""` | ✅ |
|
||||
|
||||
> **`isNewFavoriteList = "22212"` 是本次最值得注意的一处**:
|
||||
> 第 4 位是 `"1"`(而不是 `"2"`),因为服务器对 `itemKind=10`(搭档)的 Get 响应
|
||||
> 回的是 `{"itemKind":null, "itemIdList":[]}` —— 服务端怪癖。
|
||||
> 实现上用**未归一化的原始响应**作差分基线,才能复现出这个 `"1"`。
|
||||
|
||||
---
|
||||
|
||||
## 五、userData[0] 逐字段对比
|
||||
|
||||
真机 82 字段,实跑 82 字段,**命中 76/82**。
|
||||
|
||||
### 5.1 一致的关键字段(节选)
|
||||
|
||||
| 字段 | 真机 | 实跑 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `userName` | ☆Astral☆ | ☆Astral☆ | 一致 |
|
||||
| `playerRating` | 15000 | 15000 | 一致 |
|
||||
| `point` / `totalPoint` | 4110 / 52620 | 4110 / 52620 | 一致 |
|
||||
| `iconId` / `plateId` / `titleId` | 559501 / 458001 / 55502 | 同 | 一致 |
|
||||
| `charaSlot` | [395,101,400402,400401,200401] | 同 | 一致 |
|
||||
| `lastGameId` | "SDGB" | "SDGB" | 一致(兜底填充) |
|
||||
| `accessCode` | "" | "" | 一致(兜底填充) |
|
||||
| `lastPlaceId` / `lastPlaceName` | 1000 / 测试店铺… | 同 | 一致 |
|
||||
| `lastRegionId` / `lastRegionName` | 5 / 测试区域 | 同 | 一致 |
|
||||
| `lastClientId` | "TESTCLIENT1" | "TESTCLIENT1" | 一致 |
|
||||
| `lastPlayMode` | 0 | 0 | 一致(归 0) |
|
||||
| `banState` | 0 | 0 | 一致(已搬进 userData 内部) |
|
||||
| `lastRomVersion` | "1.56.00" | "1.56.00" | 一致 |
|
||||
|
||||
### 5.2 六处差异(全部可解释)
|
||||
|
||||
| 字段 | 真机 | 实跑 | 原因 |
|
||||
|---|---|---|---|
|
||||
| `playCount` | 1258 | **1259** | 基线不同:真机基线 1257,实跑基线 1258(真机那局已写入)。**增量都是 +1** ✅ |
|
||||
| `currentPlayCount` | 186 | **187** | 同上,增量都是 +1 ✅ |
|
||||
| `dateTime` | 1791134329 | **1791194138** | 本次登录的 Auth.AuthTime(不同次登录) ✅ |
|
||||
| `lastLoginDate` | 2026-10-05 00:19:37.0 | **2026-10-05 17:55:38.0** | 本次登录时刻 ✅ |
|
||||
| `lastPlayDate` | 2026-10-05 00:20:57.0 | **2026-10-05 17:55:38.0** | 本次游玩时刻 ✅ |
|
||||
| `eventWatchedDate` | 2026-10-05 00:19:37.0 | **2026-10-05 17:55:38.0** | 本次登录时刻 ✅ |
|
||||
|
||||
> **验证方式**:把实跑的 `playCount` 减 1 得 1258,正好等于本次快照基线;
|
||||
> 真机的 1258 减 1 得 1257,等于真机快照基线。**两边的增量逻辑完全一致。**
|
||||
|
||||
### 5.3 实跑 userData 全文(节选关键段)
|
||||
|
||||
```json
|
||||
{
|
||||
"accessCode": "",
|
||||
"userName": "☆Astral☆",
|
||||
"playCount": 1259,
|
||||
"currentPlayCount": 187,
|
||||
"eventWatchedDate": "2026-10-05 17:55:38.0",
|
||||
"lastGameId": "SDGB",
|
||||
"lastLoginDate": "2026-10-05 17:55:38.0",
|
||||
"lastPlayDate": "2026-10-05 17:55:38.0",
|
||||
"lastPlayCredit": 1,
|
||||
"lastPlayMode": 0,
|
||||
"lastPlaceId": 1000,
|
||||
"lastPlaceName": "测试店铺",
|
||||
"lastRegionId": 5,
|
||||
"lastRegionName": "测试区域",
|
||||
"lastClientId": "TESTCLIENT1",
|
||||
"lastCountryCode": "CHN",
|
||||
"dateTime": 1791194138,
|
||||
"banState": 0
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、userPlaylogList[0] 逐字段对比(110 字段)
|
||||
|
||||
**命中 106/110。**
|
||||
|
||||
### 6.1 四字段差异(全部可解释)
|
||||
|
||||
| 字段 | 真机 | 实跑 | 原因 |
|
||||
|---|---|---|---|
|
||||
| `playlogId` | 7692848101484135861 | 7693120247791879616 | 本次登录的 loginId ✅ |
|
||||
| `loginDate` | 1791130777 | 1791194138 | 标准 Unix 秒 = loginDateTime − 28800 ✅ |
|
||||
| `playDate` | "2026-10-04" | "2026-10-05" | **日志日**(7 点前算前一天)——真机游玩在 00:20 所以回退到 10-04;实跑在 17:55 不回退 ✅ |
|
||||
| `userPlayDate` | "2026-10-05 00:20:30.0" | "2026-10-05 17:55:38.0" | 游玩时刻 ✅ |
|
||||
|
||||
> **`playDate` 的差异恰好验证了算法正确性**:
|
||||
> 真机在 00:20 游玩 → `playDate` 回退为前一天 `2026-10-04`;
|
||||
> 实跑在 17:55 → `playDate` 保持 `2026-10-05`。
|
||||
> 这正是 `TimeManager.ToLogDateTime`(`TimeManager.cs:77-85`,7 点分界)的行为。
|
||||
|
||||
### 6.2 完全一致的 106 个字段(节选关键项)
|
||||
|
||||
| 字段 | 值 | 说明 |
|
||||
|---|---|---|
|
||||
| `userId` | 0 | 恒为 0 |
|
||||
| `version` | 1056000 | 1.56.00 |
|
||||
| `placeId` / `placeName` | 1000 / 测试店铺… | 一致 |
|
||||
| `musicId` / `level` | 143 / 4 | 一致 |
|
||||
| `trackNo` | 1 | 一致 |
|
||||
| `playerNum` | 1 | 一致 |
|
||||
| `characterId1..5` | 395,101,400402,400401,200401 | 一致 |
|
||||
| `characterLevel1..5` | 3800,4455,134,134,134 | 一致(**游玩前快照,不加经验**) |
|
||||
| `achievement` / `deluxscore` | 0 / 0 | 一致 |
|
||||
| `totalCombo` | 655 | 一致 |
|
||||
| `tapMiss` / `holdMiss` / `slideMiss` / `breakMiss` | 441 / 19 / 192 / 3 | 一致 |
|
||||
| `isTap` / `isHold` / `isSlide` / `isTouch` / `isBreak` | true,true,true,false,true | 一致 |
|
||||
| `isClear` | false | 一致 |
|
||||
| `trialPlayAchievement` | -1 | 一致 |
|
||||
| `extNum4` | 2022 | 一致(= categoryIndex) |
|
||||
| `extBool1/2/3` | false | 一致(**bool 而非 0**) |
|
||||
| `vsUserName` / `playedUserName1..3` | "" | 一致(**空串而非 0**) |
|
||||
|
||||
---
|
||||
|
||||
## 七、关键节点逐条示例
|
||||
|
||||
### 7.1 `userMusicDetailList`(差分:只回传本局那条)
|
||||
|
||||
**真机**
|
||||
```json
|
||||
[
|
||||
{ "musicId": 143, "level": 4, "playCount": 8, "achievement": 0,
|
||||
"comboStatus": 0, "syncStatus": 0, "deluxscoreMax": 0,
|
||||
"scoreRank": 0, "extNum1": 0 }
|
||||
]
|
||||
```
|
||||
|
||||
**实跑**
|
||||
```json
|
||||
[
|
||||
{ "musicId": 143, "level": 4, "playCount": 9, "achievement": 0,
|
||||
"comboStatus": 0, "syncStatus": 0, "deluxscoreMax": 0,
|
||||
"scoreRank": 0, "extNum1": 0 }
|
||||
]
|
||||
```
|
||||
|
||||
**对比**:结构、字段、键序完全一致;`playCount` 真机 7→8、实跑 8→9,
|
||||
**都是基线 +1**。Get 侧有 1037 条,这里只回传本局那 1 条 —— 差分生效。
|
||||
|
||||
---
|
||||
|
||||
### 7.2 `userMapList`(差分:只回传变化那条)
|
||||
|
||||
**真机**
|
||||
```json
|
||||
[
|
||||
{ "mapId": 550003, "distance": 1435000, "isLock": false,
|
||||
"isClear": false, "isComplete": false, "unlockFlag": 0 }
|
||||
]
|
||||
```
|
||||
|
||||
**实跑**
|
||||
```json
|
||||
[
|
||||
{ "mapId": 550003, "distance": 1446000, "isLock": false,
|
||||
"isClear": false, "isComplete": false, "unlockFlag": 0 }
|
||||
]
|
||||
```
|
||||
|
||||
**对比**:`mapId` 相同(都是当前 `selectMapId`);
|
||||
真机 1424000→1435000(+11000),实跑 1435000→1446000(+11000),
|
||||
**增量完全一致**。Get 侧有 104 条,只回传 1 条。
|
||||
|
||||
---
|
||||
|
||||
### 7.3 `userCharacterList`(差分:组队 5 只各 +1 级)
|
||||
|
||||
**真机**
|
||||
```json
|
||||
[
|
||||
{ "characterId": 101, "level": 4456, "awakening": 5, "useCount": 0 },
|
||||
{ "characterId": 395, "level": 3801, "awakening": 5, "useCount": 0 },
|
||||
{ "characterId": 200401, "level": 135, "awakening": 3, "useCount": 0 },
|
||||
{ "characterId": 400401, "level": 135, "awakening": 3, "useCount": 0 },
|
||||
{ "characterId": 400402, "level": 135, "awakening": 3, "useCount": 0 }
|
||||
]
|
||||
```
|
||||
|
||||
**实跑**
|
||||
```json
|
||||
[
|
||||
{ "characterId": 395, "useCount": 0, "level": 3802, "awakening": 5 },
|
||||
{ "characterId": 101, "useCount": 0, "level": 4457, "awakening": 5 },
|
||||
{ "characterId": 400402, "useCount": 0, "level": 136, "awakening": 3 },
|
||||
{ "characterId": 400401, "useCount": 0, "level": 136, "awakening": 3 },
|
||||
{ "characterId": 200401, "useCount": 0, "level": 136, "awakening": 3 }
|
||||
]
|
||||
```
|
||||
|
||||
**对比**:
|
||||
- 条目数都是 **5**(只回传组队角色,Get 侧有 312 条)
|
||||
- 每只都是 **基线 +1 级**:真机 4455→4456、3800→3801、134→135;实跑 4456→4457、3801→3802、135→136
|
||||
- 字段集合一致(`characterId`/`level`/`awakening`/`useCount`)
|
||||
- 唯一差别是**键序**(真机按 characterId 升序,实跑按 charaSlot 顺序)—— JSON 对象键序不影响语义
|
||||
|
||||
---
|
||||
|
||||
### 7.4 `userChargeList`(★ 整表回传,不做差分)
|
||||
|
||||
**真机**(3 条)与**实跑**(3 条)**完全相同**:
|
||||
```json
|
||||
[
|
||||
{ "chargeId": 2, "stock": 0, "purchaseDate": "2026-07-14 18:33:37", "validDate": "2026-10-12 04:00:00" },
|
||||
{ "chargeId": 3, "stock": 0, "purchaseDate": "2026-10-02 10:49:23", "validDate": "2026-12-31 04:00:00" },
|
||||
{ "chargeId": 5, "stock": 0, "purchaseDate": "2026-10-04 18:22:10", "validDate": "2027-01-02 04:00:00" }
|
||||
]
|
||||
```
|
||||
|
||||
**对比**:Get 侧 3 条 → Upsert 侧也是 3 条。
|
||||
说明 `userChargeList` 是**整表回传**,**不能**当差分节点处理
|
||||
(否则会被误收敛成空数组)。实跑已正确处理。
|
||||
|
||||
---
|
||||
|
||||
### 7.5 `userFavoriteList`(★ 恒 5 条,位串 "22212")
|
||||
|
||||
**真机**
|
||||
```json
|
||||
[
|
||||
{ "itemKind": 3, "itemIdList": [559501] },
|
||||
{ "itemKind": 1, "itemIdList": [] },
|
||||
{ "itemKind": 2, "itemIdList": [] },
|
||||
{ "itemKind": 10, "itemIdList": [] },
|
||||
{ "itemKind": 11, "itemIdList": [] }
|
||||
]
|
||||
```
|
||||
位串:`"22212"`
|
||||
|
||||
**实跑**
|
||||
```json
|
||||
[
|
||||
{ "itemKind": 3, "itemIdList": [559501] },
|
||||
{ "itemKind": 1, "itemIdList": [] },
|
||||
{ "itemKind": 2, "itemIdList": [] },
|
||||
{ "itemKind": 10, "itemIdList": [] },
|
||||
{ "itemKind": 11, "itemIdList": [] }
|
||||
]
|
||||
```
|
||||
位串:`"22212"`
|
||||
|
||||
**对比**:**逐字节一致**,包括位串。
|
||||
第 4 位 `"1"` 的来源:服务器对 `itemKind=10` 回的是 `{"itemKind":null}`,
|
||||
差分基线查不到该 kind → 位 `"1"`。
|
||||
|
||||
---
|
||||
|
||||
### 7.6 `userWeeklyData`(★ 对象,非数组)
|
||||
|
||||
**真机**
|
||||
```json
|
||||
{ "lastLoginWeek": "2026-09-28 04:00:00", "beforeLoginWeek": "2026-09-21 04:00:00", "friendBonusFlag": true }
|
||||
```
|
||||
|
||||
**实跑**
|
||||
```json
|
||||
{ "lastLoginWeek": "2026-10-05 04:00:00", "beforeLoginWeek": "2026-09-28 04:00:00", "friendBonusFlag": true }
|
||||
```
|
||||
|
||||
**对比**:结构与键完全一致。周界值差一周是因为**本次登录已跨周**
|
||||
(实跑时服务器 `GetGameWeeklyDataApi` 下发的 `updateDate` 已是 2026-10-05)。
|
||||
该值取自服务器下发的 `userWeeklyData`,不是本地推导。
|
||||
|
||||
---
|
||||
|
||||
### 7.7 `user2pPlaylog`(★ 对象,非数组)
|
||||
|
||||
真机与实跑**完全一致**:
|
||||
```json
|
||||
{ "userId1": 0, "userId2": 0, "userName1": "", "userName2": "",
|
||||
"regionId": 0, "placeId": 0, "user2pPlaylogDetailList": [] }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7.8 `userGamePlaylogList`
|
||||
|
||||
**真机**
|
||||
```json
|
||||
[{ "playlogId": 7692848101484135861, "version": "1.56.00",
|
||||
"playDate": "2026-10-05 00:20:57.0", "playMode": 0, "useTicketId": -1,
|
||||
"playCredit": 1, "playTrack": 1, "clientId": "TESTCLIENT1",
|
||||
"isPlayTutorial": false, "isEventMode": false, "isNewFree": false,
|
||||
"playCount": 8, "playSpecial": 55951502, "playOtherUserId": 0 }]
|
||||
```
|
||||
|
||||
**实跑**
|
||||
```json
|
||||
[{ "playlogId": 7693120247791879616, "version": "1.56.00",
|
||||
"playDate": "2026-10-05 17:55:38.0", "playMode": 0, "useTicketId": -1,
|
||||
"playCredit": 1, "playTrack": 1, "clientId": "TESTCLIENT1",
|
||||
"isPlayTutorial": false, "isEventMode": false, "isNewFree": false,
|
||||
"playCount": 8, "playSpecial": 16417894, "playOtherUserId": 0 }]
|
||||
```
|
||||
|
||||
**对比**:13 个字段名与类型完全一致。
|
||||
- `playlogId` / `playDate` 是本次会话值(正常)
|
||||
- `playSpecial` 两边都是**随机位掩码**(真机 55951502,实跑 16417894),
|
||||
按 `VOExtensions.cs:846-855` 的算法生成,每次不同 —— 正确
|
||||
- `useTicketId=-1`(不使用票)、`playCredit=1`、`playCount=8` 全部命中
|
||||
|
||||
---
|
||||
|
||||
## 八、差异汇总与判定
|
||||
|
||||
| 类别 | 差异项 | 判定 |
|
||||
|---|---|---|
|
||||
| 顶层 | `playlogId`、`loginDateTime` | ✅ 正常(每次登录不同) |
|
||||
| userData | `playCount`、`currentPlayCount` | ✅ 正常(基线不同,**增量都是 +1**) |
|
||||
| userData | `dateTime`、`lastLoginDate`、`lastPlayDate`、`eventWatchedDate` | ✅ 正常(本次会话时刻) |
|
||||
| playlog | `playlogId`、`loginDate`、`playDate`、`userPlayDate` | ✅ 正常(本次会话时刻 + 日志日算法) |
|
||||
| 结构 | 无 | ✅ **36 节点形态、11 位串全部一致** |
|
||||
|
||||
**没有任何一处是「实现错误」导致的差异。**
|
||||
|
||||
---
|
||||
|
||||
## 九、本次实跑发现并修复的 3 个 Bug
|
||||
|
||||
| # | 问题 | 根因 | 修复 |
|
||||
|---|---|---|---|
|
||||
| 1 | `userData.dateTime` 为 `null` | `UserLogin` 没有回填它实际发出的 `dateTime` | `UserLoginResponseVO` 增加 `dateTime` 字段并在登录时回填 |
|
||||
| 2 | `userFavoriteList` 为空、位串为 `""` | 取数脚本返回的字典用**节点名**作键(`userFavoriteList`),而原始响应用 `userFavorite`;提取逻辑只认后者 | 提取时增加按节点名兜底 |
|
||||
| 3 | `userWeeklyData` 周界比真机晚一周 | 用了本地推导而非服务器下发值 | 优先从 `GetUserMissionDataApi` 响应的 `userWeeklyData` 取(`UserMissionResponseVO.cs:10`) |
|
||||
|
||||
修复后重跑,**结构差异归零**。
|
||||
|
||||
---
|
||||
|
||||
## 十、怎么复现这次对比
|
||||
|
||||
```powershell
|
||||
# 1) 实跑(dry-run,不发包)—— 把二维码换成你自己的
|
||||
python upsert.py "<二维码URL>"
|
||||
|
||||
# 2) 只看构造出的报文(不联网)
|
||||
python -c "import json;d=json.load(open('upsert_payload.json',encoding='utf-8'));print(json.dumps(d['upsertUserAll']['userData'],ensure_ascii=False,indent=2))"
|
||||
|
||||
# 3) 零网络自检(129 项)
|
||||
python selftest.py
|
||||
```
|
||||
|
||||
> **注意**:本次实跑**没有真实发送** UpsertUserAll。
|
||||
> 要真实上传需显式加 `--send`(会把成绩写进服务器,不可撤销)。
|
||||
|
||||
---
|
||||
|
||||
## 十一、相关文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|---|---|
|
||||
| `api/upsert_user_all.py` | 上传成绩实现(36 节点 / 110 字段 / 差分) |
|
||||
| `api/upsert_snapshot.py` | 结算快照编排 |
|
||||
| `docs/上传成绩开发文档.md` | 字段级开发文档 |
|
||||
| `source/2026-10-05.log:123` | 真机报文(本文档对比基准) |
|
||||
| `docs/UpsertUserAll实跑对比.md` | 本文档 |
|
||||
Reference in New Issue
Block a user