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

18 KiB
Raw Blame History

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 条。


二、顶层结构对比

// 实跑产出(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 全文(节选关键段)

{
  "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(差分:只回传本局那条)

真机

[
  { "musicId": 143, "level": 4, "playCount": 8, "achievement": 0,
    "comboStatus": 0, "syncStatus": 0, "deluxscoreMax": 0,
    "scoreRank": 0, "extNum1": 0 }
]

实跑

[
  { "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(差分:只回传变化那条)

真机

[
  { "mapId": 550003, "distance": 1435000, "isLock": false,
    "isClear": false, "isComplete": false, "unlockFlag": 0 }
]

实跑

[
  { "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 级)

真机

[
  { "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 }
]

实跑

[
  { "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 条)完全相同:

[
  { "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")

真机

[
  { "itemKind": 3,  "itemIdList": [559501] },
  { "itemKind": 1,  "itemIdList": [] },
  { "itemKind": 2,  "itemIdList": [] },
  { "itemKind": 10, "itemIdList": [] },
  { "itemKind": 11, "itemIdList": [] }
]

位串:"22212"

实跑

[
  { "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(★ 对象,非数组)

真机

{ "lastLoginWeek": "2026-09-28 04:00:00", "beforeLoginWeek": "2026-09-21 04:00:00", "friendBonusFlag": true }

实跑

{ "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(★ 对象,非数组)

真机与实跑完全一致:

{ "userId1": 0, "userId2": 0, "userName1": "", "userName2": "",
  "regionId": 0, "placeId": 0, "user2pPlaylogDetailList": [] }

7.8 userGamePlaylogList

真机

[{ "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 }]

实跑

[{ "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)

修复后重跑,结构差异归零。


十、怎么复现这次对比

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