Files
ft/CodexOutput/upserver/阶段2独立KV代码审查-2026-09-22.md

132 lines
13 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.
# 阶段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。
## 阶段放行条件
1. 完成上述 JSON 严格程度裁决;如按标准 JSON,修复后补失败回归并限定复审。
2. 独立测试账号固定 Cookie/HttpClient,专用唯一前缀 key 验证读写删除、同名 key 账号隔离与重新登录持久化。
3. finally 删除本轮 key,并对两个账号再次确认不存在;清理不成功须明确记录,不能仅依赖接口调用已返回。
4. 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 删除、最终打包/真机不属于此次验收,不因本报告通过自动开始或标记完成。