Files
Astral/docs/UpsertUserAll实跑对比.md
T
Txiaochen 41909b9169 docs: WaHappy 开发文档
孤儿分支,只含 docs/。内容包括:
- 项目总览 / 主脚本编写指南 / 日志指南
- 上传成绩开发文档 / UpsertUserAll 踩坑清单(28 条)
- 真机 Hook 对照报告 / 实跑验证记录
- 购买功能票流程(含被服务端拒绝的排查过程)
  · 扫码登录协议技术规范
2026-10-06 14:42:32 +08:00

488 lines
18 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.
# 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` | 本文档 |