# M2 C3c 本人统计 KV 暂存实施增量 日期:2026-09-30。状态:**开发侧方案核查完成,待现有 C3c 集中审查裁决;未实施。** 本增量落实用户“KV 先存一份,真正联调时再确定最终读取”的方向,补充《M2-先存KV后联调的阶段移植方案-2026-09-30.md》。与原 C3c 同次审查、同份报告;不重新审已批准的 C2d/C3b,不再询问迁移方向、身份或邮件流程。原 C3c 文件及 hash 保留,不重写历史结论。 ## 1. 具体改动和数据来源 生产修改仅限 `Client/Assets/Scripts/DataCenter/DataCenter.LeadboardData.cs` 的 `SetLeadboardData`、`SetLvData` 及必要 using。 | 位置 | 原调用 | KV 接线候选 | 必须保留 | |---|---|---|---| | SetLvData | 计算复合分数后,await SetLeadboardData | 仍在原位置传入分数;未知地区仅传全球键 | async void、isRefreshing 的原赋值位置、全部计算与已有调用者 | | SetLeadboardData | 构造统计列表,await UpdatePlayerStatisticsAsync | 同一列表写到当前 KVUserService.SaveStore,合并等待一次 FlushAsync | Task 返回类型;正常完成及已知请求失败均结束等待;错误仍可见 | | 非 KV 路线 | PlayFab 统计请求 | 原实现原样执行 | PF/AGG 使用方式、顺序和 SDK 参数 | 不新增注入、接口、管理器、缓存、运行开关或静态全局 store。使用已有 `userService is KVUserService` 判断;进入 KV 分支立即将 `kvUser.SaveStore` 捕获到局部变量,await 后不重新获取新会话。底层 KVMgr 不增加榜单业务依赖。 键值如下: - `LevelB`:原计算结果的 `ToString(CultureInfo.InvariantCulture)`,十进制整数字符串。 - `LevelB`:同一分数。KVUserService 已将地区归一为 AF/AN/AS/EU/NA/OC/SA 或 null;直接消费该来源,不复制校验表、不猜地区。 - KV 地区为 null/空时不创建地区项,尤其不把空前缀拼成第二个 `LevelB`。这落实已审“未知地区不冒充全球地区榜”的边界;PF 的原地区行为保持。 - `LastLoginTimeUtc`:已有实现复用,本增量不改代码、不新增 `LastLogin` 键。 原分数算法逐行保留:建筑字符串 int.TryParse;建筑值达到 100000 时压到 99999;玩家 lv 高于配置最后一项 LevelRequired 时按原逻辑回写上限;分数为 lv×100000+建筑值。不新增负值归零、溢出处理或新校验,不修原数据缺陷。LevelB 快照不参与反向恢复 lv,也不成为新的等级真值。 ## 2. 精确调用顺序和结果处理 `SetLeadboardData` 保留现有先构造完整 `List` 的循环。这样参数长度错误仍在写入前暴露,不因 KV 逐键写而提前产生副作用;本增量不删除仍在使用的 PlayFab 数据模型。列表构造完成后、SDK 调用前增加 KV 分支: 1. 捕获该用户的原 store。依次对统计列表调用 `store.Write(name, decimalValue)`,采用默认 remoteStore=true。 2. 每个不接受结果记录 `Debug.LogError`,内容包括统计键与错误状态,不包含存档正文、Cookie 或凭据;不调用 RequireAccepted,不转成新异常。 3. 若至少一项已接受,调用并 await **一次** `store.FlushAsync()`。所有项均未接受时,记错后直接返回;不额外刷新无关存档。 4. Flush 返回 Confirmed 则结束;Rejected/Unknown/Cancelled/Superseded/Buffered/LocalOnly 均记错误状态,然后结束。Unknown 不写成成功、不自动重试、不回退 PlayFab、不新建会话。 5. 返回原调用者,继续执行原 `isRefreshing=false`。不新增 finally、不加重入锁、不改变 async void。意外编程异常仍按原异常路径传播,不用宽泛 catch 吞掉。 6. 非 KV 分支保留原 UpdatePlayerStatisticsRequest 和 await SDK 代码。 **失败语义依据**:`Client/LocalPackages/asap.playfab.async/Runtime/Gen/PlayFabClientAsyncAPI.cs:2430` 的 UpdatePlayerStatisticsAsync 在错误回调中执行 Debug.LogError,然后 task.SetResult(null);原 SetLeadboardData 忽略该返回值。因此不能把 KV 的明确拒绝或未知结果改成抛异常/阻断后续业务,也不能以 Task 正常结束宣称远端保存成功。通过日志和测试产物区分结果,保持原公开返回合同。 ### 现有存储能力的限制,需在本次审查明确接受 - `KVPlayerSaveStore.FlushAsync` 会刷新该固定 handle 的待同步内容,不只本次统计键;它还检查捕获的存档凭据。因此既有其他待写项失败也可能使本次返回非 Confirmed。复用现有合同,不新增“统计专用刷新”接口、锁或第二个 handle。 - 多键 Write/Flush 不提供事务原子性。全球/地区快照可能部分接受、部分确认;不回滚或伪造两键同时成功。两个值均由同一次原分数计算得到,下一次原刷新按原触发点重写,不新增补偿循环。 - 并发更新/注销由已有 store 处理;本增量不增强原业务并发规则。不确认到新账号,不把 Superseded 当本次已确认。 - 本人暂存不是聚合榜实现,真实 KV 读取接口能否消费此键仍待联调。这是阶段方案限定接受的存储差异,不能以本次通过解除场景闸或宣布等级榜迁移完成。 若审查认定这些限制不符合暂存边界,应一次给出具体不通过项;不得自行扩展底层事务或更改业务流程规避。 ## 3. 登录时间保持现有流程 `KVUserService.PrepareProfile` 读取已有 LastLoginTimeUtc 作为前次时间,当前 LoginTime 来自认证服务器时间。`FishingStage` 完成每日初始化后先保存/刷新原档,再保存当前 UTC 往返格式 O 时间。不得为了好友查询提前覆盖前次时间。 正式 101/102 的已归档样例尚无该键,本轮不补写假历史时间。好友时间最终读 KV 还是账号数据库由真实联调确定;缺值语义和投影接线继续列在 C3c,不用客户端改排序或补当前时间掩盖。 ## 4. 测试文件边界与验证方法 复用 `CodexOutput/upserver/KVGameLoadTests/ServiceHost/`,候选范围如下;不为测试抽取新的生产层。 | 文件 | 允许变更 | |---|---| | ServiceHost.csproj | Compile 链接完整真实 DataCenter.LeadboardData.cs,不复制/截取方法 | | Stubs.cs | 补完整类编译所需配置、UI、事件的最小签名;PlayerData 补 InfiniteBuildingLevel;SDK 夹具加统计请求计数、参数记录及可控完成任务 | | C3IdentityCompatibilityTests.cs | 删除 game.LeadboardData 常量替身,让 FriendService 使用真实 GameCore.LeadboardData;现有 LeadboardItemData 替身仅补 SetData 等编译签名,不测试其伪实现 | | Program.cs | 接入同一离线执行序列;沿用上下文泵驱动真实 async void 完成 | | C3RemainingSdkTests.cs(新增候选) | 调用真实 LeadboardData;存储结果夹具、真实 KV store/handle 与合成传输用例 | 完整 LeadboardData 包含活动奖励/配置依赖,必须让整文件通过宿主编译;未运行的活动方法依赖只提供编译签名,意外调用应明确失败,不能默认为业务成功。本批结果只声称统计写入覆盖。链接导致既有测试名称解析冲突时,只调整上述测试文件的类型限定名;不改生产业务依赖。 最小用例集(全部为待执行,不是已通过): 1. KV 全球+AS 地区:真实 SetLvData 输入 lv=12、建筑=34,写出两个 `1200034`,确认前保持原等待状态、确认后复位;KV SDK 计数为0。 2. 地区缺失:只写一次 LevelB;已有合法地区两项写入顺序全球在前。不新设地区开关。 3. 原计算:建筑99999/100000/坏字符串、lv超过配置上限;验证实际 SetLvData 输出和原 lv 回写。负值等原缺陷不在本批修正。 4. SetLeadboardData 参数数量不匹配:与原循环一致,在任何 store.Write 前失败;不增加新的参数合同。 5. 明确拒写、全部拒写、部分接受:错误可观察,无 PF 回退;至少一项接受才 Flush;部分失败不报告两键都已确认。 6. Flush 的 Confirmed、Unknown、Rejected、Cancelled、Superseded:等待结束和错误日志符合第2节;不新增 retry,不让已知结果把原 isRefreshing 卡住。 7. 延迟确认时注销/换号:真实 store 捕获旧 handle,旧操作不得写新账号或以新会话确认;同键更新与其他待写项失败按现有 store 结果记录。 8. 真实 KV store + 合成远端:检查发出的本人 UserID、键值、同步响应及新会话重新读取;不是只检查本地缓存。覆盖多键部分失败。每组标明 formalRequests=0。 9. PF、ANDROID+AGG 对照:原两项 StatisticUpdate、原 SDK 次数、Task 结束时序保持;PF 错误夹具模拟原包装器的 log+null 完成,注明包装器本身另由源码核查,并非实际联网 PF 验证。 10. 运行全部既有 ServiceHost 用例,确保引入完整真实类后 C3b 好友身份用例仍通过;Unity CLI Loop 编译验证实际配置/UI 类型,无 Play、无打包。 命令沿用 `dotnet run --project CodexOutput/upserver/KVGameLoadTests/ServiceHost/ServiceHost.csproj --no-restore` 及 `-p:AndroidProbe=true`;Unity 只用 `uloop compile --project-path C:/projects/ft/Client --wait-for-domain-reload true`。真实账号 KV 写入另在明确联调检查点执行,本次审查通过不等于允许绕过阶段闸自动进入场景。 ## 5. 开发侧核查裁决及审查接续 | 项目 | 开发侧结论 | 独立审查需要裁决 | |---|---|---| | 用户方向 | 已明确允许先暂存,覆盖旧“不新增LevelB键”的建议 | 不重问方向,只核最小实施是否符合 | | 抽象必要性 | 无新增生产抽象;一个文件、两个方法即可完成本人统计替换 | 认可复用 KVUserService.SaveStore 与既有批量刷新限制 | | 业务时序/错误 | 保留原 await 和 log 后正常完成;撤销任何 RequireAccepted/throw 提议 | 核失败矩阵与原包装器一致 | | 地区 | 使用已有已校验 ContinentCode,缺失不造第二个全球键 | 核沿用已审地区边界,PF 不改 | | 时间 | 已有实现复用,本次零变更 | 不引入重复键或提前写入 | | 临时数据 | 原 lv/建筑仍唯一计算源;快照不能充当全服查询 | 接受阶段限制,最终确定消费者或回收闲置写入 | | 测试 | 完整真实类入宿主,双条件回归+CLI Loop;无生产测试抽象 | 核测试五文件边界及合成证据限制 | **给原 ZCode 的同次审查补充**:先核原 `M2-C3c剩余SDK差异受审hash.json`,再核 `M2-C3c本人统计KV暂存增量hash.json`;结合非空好友线上事实、服务器只读核查报告及阶段方案,继续写到原约定的 `M2-C3c剩余SDK接线独立差异审查-2026-09-30.md`。列出本增量是否批准、逐项限制和允许文件;D1–D5中跨玩家事实未齐的部分明确保持未裁决,不阻挡已完整裁决的本人存储实施,也不能给未定义部分空泛PASS。不修改服务器源码,不要求重审已批准同合同内容。 收到报告后主会话核 hash、逐条记录正式裁决;通过范围内连续写测试、编码、验证并更新检查点。当前没有该报告,本文件的开发侧核查不冒充独立批准。服务器、Core/KVMgr、现有 store、KVUserService、FishingStage、好友/UI消费者、debugconfig 和场景资产均不在本增量生产修改范围。 ## 6. 最终清理与当前事实 LevelB/地区快照登记为迁移期数据;联调后确定实际消费者。确无消费者时,仅移除本次造成的闲置实现;历史 KV 数据清理另按最终批准方案,不自行删档。双闸、匹配暂停等全部临时路径最终恢复并验证;当前继续保留未完成标记。命名统一与原业务缺陷等整个迁移结束后另行处理。 本检查点仅完成方案、待审基线和状态文档;没有生产/测试代码修改,没有运行新业务测试,没有线上请求、KV写入、服务器修改、Play或打包。