13 KiB
阶段2独立 KV 代码审查
日期:2026-09-22。独立审查任务:login_rounds_independent_review。
结论
新增协议客户端对有效服务端契约的 Get/Set/Delete、参数快照、错误区分、取消、写结果未知和调用方 HttpClient 所有权处理符合当前阶段方案,未确认其中的 P0/P1 阻塞缺陷。42 项本地测试直接编译生产源码,已保存全部通过日志。
有一项协议严格性边界需要明确裁决:Json.NET 默认扩展语法也会被当作成功的 Get。 若本阶段所称「严格完整 JSON 对象」要求标准 JSON 语法,则目前尚不能称这项通过,建议补测试并修复;若明确允许 Json.NET 扩展语法,需把该容忍写进协议边界。此项不影响按当前服务端序列化的有效响应,也不阻止进行隔离测试 key 的真实联调;不得据本报告跳过账号隔离、清理和持久化验收。
本审查未修改生产代码或主会话测试,未运行 Unity、未调用网络,也未接入游戏存档。打包、生产桥接、缓存同步、PlayFab 替换/删除继续在后续阶段。
审查输入和版本
- 已读《阶段确认与阶段2启动裁决-2026-09-22.md》。
- 完整阅读
PlayerKVModels.cs、PlayerKVClient.cs、KVContractTests/Program.cs、csproj 和 Latest.log。 - 只读核对服务端
PlayerDataApi.cs、PlayerKVModel.cs、PlayerKVService.cs、PlayerKV 迁移。服务端 HEAD 为858fe7c922acca77058afc9c5f31518b52e04dfc,三个契约文件无 diff。仓库所有权检查通过单条命令的-c safe.directory处理,未修改 Git 全局配置。
| 审查文件 | SHA256 |
|---|---|
| PlayerKVModels.cs | 836B1A20F0510D48E74AD8D25AEA4E0DAAB3B6E6887374FDEBFDC0ADA688FE4D |
| PlayerKVClient.cs | 9D2D9FC38A0EE48F2CB0B926BB733621045FE179C5B20E7717B0F975D415D2FE |
| KVContractTests/Program.cs | 7DCA39AC8377FB7E942A057B4CF21083586362911F22DBAB1049DC40E63483F0 |
协议严格性发现:扩展语法可成为成功空档(条件性 P2)
位置: Client/Assets/Scripts/Services/PlayerData/PlayerKVClient.cs:130,JsonTextReader / JObject.Load。
通过纯本地内存调用当前 KVContractTests.dll 中真实 PlayerKVClient.ParseData,确认:
| 正文 | 实际解析 |
|---|---|
{data:{}} |
接受,得到空 data |
{"data":{},} |
接受,得到空 data |
{"data":{"x":"v",}} |
接受,得到字符串值 |
{"data":{} |
JsonReaderException |
{"data":{"x":"v"} |
JsonReaderException |
{"data":{}} trailing |
JsonReaderException |
{"data":{}} /*comment*/ |
JsonReaderException |
前 3 项不是标准 JSON,原因是 Json.NET 默认支持未引号属性名、尾逗号。当前的 DuplicatePropertyNameHandling.Error 和 reader.Read() 正确解决了重复属性/多个根对象/尾随内容,却不收紧这些语法。SendAsync 在 HTTP 200 下会把 ParseData 的正常返回映射为 Success,故无效标准 JSON 的响应可被当作空存档,而非 Protocol。现有 42 项覆盖了缺 data、非法值、重复属性与尾随第二对象,未覆盖以上扩展语法。
建议: 主会话先就严格程度裁决;若保持标准 JSON 要求,新增上述失败用例,使用不改字符串内容的完整语法验证后再做现有 data 结构检查。不要通过粗暴删除注释/逗号或正则替换正文「修好响应」,以免改动存档值。不得为此引入未经评审的新包或扩大生产存档迁移。
证据限制: 此操作只调用已构建的实际解析方法,没有正式网络、账号读写或合成运行时授权。独立审查未改动 42 项测试,也未假装它们已覆盖标准 JSON 全部语法。
已独立核对的正确性边界
| 项目 | 核对结果 |
|---|---|
| 请求契约 | POST playerdata/Get/Set/Delete;Get null/空 keys 发送 {};Set data、Delete keys 字段匹配服务端;不携带 playerId 绕过 Cookie 身份。 |
| 参数 | 空/超长 key、null 写入值拒绝;128 UTF-16 长度边界、空字符串值允许。空 Set/Delete 仍发送,因此不在未授权情况下本地伪造成功。 |
| 请求快照 | Get/Delete 先复制枚举;Set 先复制 JObject;SendAsync 在首个等待前序列化请求内容。调用返回后原 List/Dictionary 变化不会影响发送。不是并发修改同一枚举器的线程安全承诺。 |
| 数据结果 | 成功 Get 才有只读 Data;空正文、缺 data、非对象、非字符串值等不返回空字典假成功;日期形字符串不自动转日期;重复属性拒绝。 |
| 写入结果 | 仅 HTTP 200 成功,正文不做业务解析;失败写请求进入传输后 MayHaveCommitted=true,包括保守的 HTTP 失败。没有自动重试/重登,不把异常推导成一定未写入。 |
| 取消 | 预取消不调传输;传输返回后检查取消,读取结果后再检查,迟到成功不发布。TaskCanceledException 按调用 token 区分 Cancelled/Timeout。 |
| 错误 | 401/403→Unauthorized;其它非 200→Http;HTTP/IO异常→Network;解析异常→Protocol;不保留完整正文或异常细节到结果。传输已 Dispose 作为取消处理符合调用方生命周期约定。 |
| 会话所有权 | 构造时固定 readonly HttpClient/绝对基址;不用全局 CustomServerMgr,不改 BaseAddress/DefaultRequestHeaders,不 Dispose 注入客户端。请求和响应正常路径均 using。 |
| 返回数据不可变 | ReadOnlyDictionary 包装 ParseData 内部新建字典,无对外可变字典别名,字符串值不可变。 |
| 有效服务端语义 | 服务器按 NameIdentifier 解析十进制身份并按 PlayerId 查询;Set/Delete 空集合无操作、正常空正文 200;不存在 key 不返回;未提供 revision/CAS/LastUpdated,客户端没有伪造。 |
测试证据与仍需联调的事项
KVContractTests.csproj直接编译两个生产文件,并引用项目实际 Unity Newtonsoft.Json 程序集;不是一套协议镜像实现。Latest.log 记录 42 项通过。本审查阅读源码与历史运行结果,没有另行重跑/覆盖日志。- HttpMessageHandler 替身验证请求体/错误/取消/资源释放,不能证明实际 Cookie 附带、账号隔离、部署契约、数据库持久化或清理成功。这些由主会话独立宿主 101/102 专用 key 联调取得证据。
- 固定会话是调用方所有权协议,而非此类封装强制锁住 CookieContainer。宿主必须独立 CookieContainer 和 HttpClient、禁用自动重定向、禁止把同一 transport 换到另一账号;结束时先取消/等待请求,再 Dispose。不能拿客户端 readonly 字段证明 Cookie 本身不可变。
- 测试 handler 直接抛出取消异常验证映射,不等于真实 HTTP 超时计时或平台传输行为;Unity 兼容性编译由主会话执行。
- 默认 HttpClient.SendAsync 会缓冲响应正文后返回;Set/Delete 虽然不解析正文,仍可能因响应体传输失败返回保守的 Network/Timeout + MayHaveCommitted。当前服务端正常 200 空正文与此兼容;不得宣称只接到响应头就已确认成功。如果未来改 ResponseHeadersRead,需要重新设计 Get 读取正文的取消与超时,不可只换一行。
- MySQL 迁移未显式指定 K 列排序规则。客户端 Ordinal 只说明返回字典保留键原文,不证明服务器把 A/a 当独立 key。本阶段不要宣称已验证大小写键隔离,也不要为该未定需求改服务器或全局规范化 key。
阶段放行条件
- 完成上述 JSON 严格程度裁决;如按标准 JSON,修复后补失败回归并限定复审。
- 独立测试账号固定 Cookie/HttpClient,专用唯一前缀 key 验证读写删除、同名 key 账号隔离与重新登录持久化。
- finally 删除本轮 key,并对两个账号再次确认不存在;清理不成功须明确记录,不能仅依赖接口调用已返回。
- Unity 编译通过且当前正式基址正确;未接入游戏业务存档。
本文件完成独立静态审查,不单独宣布阶段2全部验收通过。
补充终审:JSON 边界裁决及正式联调证据复核
2026-09-22 同日补充,本节覆盖上文「JSON 边界待裁决/真实联调待完成」的历史状态。本次仅只读复核并更新本报告,未重新操作 Unity 或正式服务器。
1. JSON 条件性 P2 已完成边界裁决
已读取《开发日志.md》“阶段2步骤2”的明确裁决:本阶段沿用项目 Json.NET 扩展语法兼容;“严格”只指完整单根对象、data 字符串字典、重复字段和尾随内容拒绝,不声称 RFC 严格语法。未引号键和尾逗号允许,阶段4存档接入前另审空档初始化/覆盖条件。
独立复核结论:该裁决与当前实际实现一致,原语法发现仍是被明确接受的兼容边界,不再作为本批阶段2阻塞项。没有通过改写历史测试或新增手写解析器隐去原行为;后续不得把成功空字典直接等价为可以清空/覆盖已有存档。
2. 独立联调宿主范围核对
已完整读取 KVLiveTests/Program.cs、csproj、Latest.log、Latest-result.json 和本次 manifest。
- 目标与账号固定: API 常量为
http://s101.local:18080/;Open 只允许 101/102,并验证返回身份一致、不是新创建账号。 - 固定会话真实实现: 每次 Open 新建 S101AccountSession;认证成功后把该实例 CookieContainer 交给专属 HttpClientHandler,UseCookies=true、AllowAutoRedirect=false,15 秒超时;没有共用游戏全局 manager 或把现有 KV transport 换号。
- 请求完成与所有权: 所有调用都顺序 await,没有后台写入或自动写重试;using 结束前本次请求已返回,再取消、Dispose 专属 HttpClient/认证会话。Dispose 的同步实现不能作为任意异步调用方的通用等待保证,但符合此宿主实际控制流。
- 专用写删边界: manifest 在可能写入前保存;API、101/102 和四个精确后缀严格校验。先在两个账号检查四 key 均不存在,再置 CleanupAllowed,之后才执行写入。碰撞/前置失败时不会授权删除已有同名 key。
- 业务数据: 全量 Get 仅在内存保留并比较非 fixture 数据,没有输出真实值;全部 Set/Delete 仅使用 manifest 四 key 或空集合,没有业务 key 写删。无 Cookie 的 Set/Delete 使用空集合,避免认证失效测试误写数据。
- 清理: finally 对两账号重新登录,Delete 精确 key 后 Get 精确 key,并断言 Count=0;每次清理先移除旧 CleanedAccounts 记录再重验,不沿用上次成功。写结果未知会设置持久 HadUnknownWrite,使最终 Completed 不能变 true。
3. 本轮实际证据一致
本次唯一前缀为 codex_kv_s2_20260922_3bc01858f94e4f67b984aeaae018236d,四后缀为 _shared、_empty、_json、_missing。其中 _missing 用于验证不存在 key 和删除无操作;清理包含它并不会扩大业务范围。
| 证据 | 复核结果 |
|---|---|
| KVLiveTests/Latest.log | 16 条 PASS,Endpoint 正确,最终 completed=True |
| KVLiveTests/Latest-result.json | passedCount=16、completed=true、cleanupVerified=true、HadUnknownWrite=false;成功条目与日志一致 |
| 本次 manifest | Accounts 精确 101/102;Keys 精确四个专用 key;CleanupAllowed=true;CleanedAccounts 同时有 101/102;Completed=true;HadUnknownWrite=false |
| 账号隔离与持久化条目 | 同名 fixture 在两个独立 Cookie 中读回各自值;101 更新/删除不影响102;两个账号重新登录并建立新 Cookie/HttpClient 后均读回预期 fixture |
| finally 清理条目 | 101、102 均有 Delete 后的不存在读回;两账号非 fixture KV 前后内存比较一致 |
| kv-unity-compile.json | Success=true、ErrorCount=0、WarningCount=274;不表述为零警告 |
这里的“持久化”已证明跨新登录/Cookie/HttpClient 可读回,不额外声称执行过数据库/服务重启或跨设备测试;“原业务 KV 未变化”是本轮前后字符串字典比较结果,不是游戏 PlayFab 存档迁移或完整玩法验收。
4. 本次复核证据 hash
| 文件 | SHA256 |
|---|---|
| KVLiveTests/Program.cs | 09823E73095B5CA2601A6A768FB8AF2487E58986754DD6603A4AA78CB41FB835 |
| KVLiveTests/Latest.log | A19B03E61969235293EBE4665F05B5ED86D0EDFAEEF509F320BD6BE9DBC30A46 |
| KVLiveTests/Latest-result.json | B0D313EB31B0E4FFEB372ED16BC1835E21D5FF4E09F7CC36F5C185988BED2283 |
| codex_kv_s2_20260922_3bc01858f94e4f67b984aeaae018236d.manifest.json | F4E18050C4220CFE7F0C724D2BFEB426EF085A647DE51257CF79F455A4814A0F |
5. 最终限定裁决
在已明确的 Json.NET 兼容边界下,本批独立 KV 协议适配代码及现有联调/清理证据复核通过,没有确认的阶段2剩余阻塞项。 上文四个放行条件现有对应裁决/16项正式联调/清理读回/Unity编译证据,不再全部列为未完成。
此结论仅放行已实施的独立 Get/Set/Delete 适配范围。固定生产会话桥接、缓存和可靠同步、空档覆盖策略、游戏登录/玩法存档切换、SDK 删除、最终打包/真机不属于此次验收,不因本报告通过自动开始或标记完成。