# 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` | 本文档 |