Files
ft/CodexOutput/upserver/阶段4C2扣费前拦截诊断方案独立审查-2026-09-24.md

123 lines
16 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.
# 阶段4C2扣费前拦截诊断方案独立审查
2026-09-24。独立评审agent输出。审查对象:《阶段4C2扣费前拦截诊断最小方案-待评审-2026-09-24.md》(sha256 2877E083…F249)。本审查为只读审查,除本报告外未写任何文件,未修改任何共享文档/TODO/开发日志/生产代码,未运行Unity,未连接正式服务器或测试账号。
## 一、审查范围与方法
### 1.1 读过并核对的文件
- 方案与记录:《阶段4C2扣费前拦截诊断最小方案-待评审-2026-09-24.md》、《阶段4C2首次真实尝试与扣费前拦截记录-2026-09-24.md》、《其他AI审查提示词-4C2扣费前拦截诊断-2026-09-24.md》、《接手入口.md》(第1~60行当前状态与硬约束部分及全文关键词检索)。
- 生产源码:`Client/Assets/Scripts/GampPlay/FishingB/FishingBehaviorB.cs`(第1~100行、第195~210行、第795~810行)、`Client/Assets/Scripts/GampPlay/FishingB/FishingData.cs`(全文)、`Client/Assets/Scripts/Tables/Item.cs`(ID/SubType声明)、`Client/Assets/Scripts/Tables/FishData.cs`(ID声明)、`Client/Assets/Scripts/Services/UserService.KVGameplayLoop.cs`(第180~424行)、`Client/Assets/Scripts/Services/UserService.KVFishingReward.cs`(PrepareFishingReward相关行)、`Client/Assets/Scripts/Services/UserService.KVFishingValidation.cs`(全文,Editor固定测试)。
- 测试与证据:`KVFishingRewardTests/ConsumeControlSlice.cs.txt`、`ConsumeControlTests.cs.txt`、`ConsumeControlTests.csproj`、`KVGameplayLoopTests/runs/42620_639258392922105429/events.jsonl`、`KVFishingRewardTests/real-attempt-final-diff.json`、`real-after-attempt-errors.json`、`real-login-reconciliation.json`、`real-final-idle-state.json`、`real-precast-conditions-retry.json`、两份102备份(094234Z/094501Z)。
- 独立验证:用powershell Get-FileHash逐项计算18项SHA256(大写hex)与字节数;用ConvertFrom-Json对两份102备份做45键逐值比对;全库检索固定错误串"Only one ordinary paid fishing action"确认无逻辑依赖该文本。
### 1.2 hash核对结果表(基目录 C:\projects\ft,Get-FileHash SHA256 大写hex + 字节数)
| # | 文件 | 期望hash(清单) | 实算hash | 期望/实算字节 | 结论 |
|---|---|---|---|---|---|
| 1 | Client/Assets/Scripts/GampPlay/FishingB/FishingBehaviorB.cs | 0A76743E…9BE8 | 一致 | 99010/99010 | 一致 |
| 2 | Client/Assets/Scripts/GampPlay/FishingB/FishingData.cs | 8C7DC3D3…0A4 | 一致 | 8230/8230 | 一致 |
| 3 | Client/Assets/Scripts/Services/UserService.KVGameplayLoop.cs | 930D1FF0…B4A | 一致 | 40556/40556 | 一致 |
| 4 | Client/Assets/Scripts/Services/UserService.KVFishingReward.cs | 3104B335…D101 | 一致 | 20005/20005 | 一致 |
| 5 | CodexOutput/upserver/阶段4C2扣费前拦截诊断最小方案-待评审-2026-09-24.md | 2877E083…F249 | 一致 | 3020/3020 | 一致 |
| 6 | CodexOutput/upserver/阶段4C2首次真实尝试与扣费前拦截记录-2026-09-24.md | 75613397…3EB6 | 一致 | 4464/4464 | 一致 |
| 7 | CodexOutput/upserver/其他AI审查提示词-4C2扣费前拦截诊断-2026-09-24.md | EADC12D4…994F | 一致 | 896/896 | 一致 |
| 8 | CodexOutput/upserver/接手入口.md | 79A0D1AC…2F58 | 一致 | 57695/57695 | 一致 |
| 9 | CodexOutput/upserver/KVFishingRewardTests/real-after-attempt-errors.json | 657BF9AD…61B4 | 一致 | 3530/3530 | 一致 |
| 10 | CodexOutput/upserver/KVFishingRewardTests/real-attempt-final-diff.json | 0BA88ED4…3203 | 一致 | 397/397 | 一致 |
| 11 | CodexOutput/upserver/KVFishingRewardTests/real-login-reconciliation.json | 2B116C7F…7E33 | 一致 | 905/905 | 一致 |
| 12 | CodexOutput/upserver/KVFishingRewardTests/real-final-idle-state.json | 0636C241…9456 | 一致 | 830/830 | 一致 |
| 13 | CodexOutput/upserver/KVFishingRewardTests/real-precast-conditions-retry.json | AF2A05F8…B3F1 | 一致 | 1236/1236 | 一致 |
| 14 | CodexOutput/upserver/KVGameplayLoopTests/runs/42620_639258392922105429/events.jsonl | F6DC9FF1…0B03 | 一致 | 1872/1872 | 一致 |
| 15 | CodexOutput/upserver/KVGameplayLoopTests/backups/20260924T094234Z/102.json | A9391829…0A23 | 一致 | 14351/14351 | 一致 |
| 16 | CodexOutput/upserver/KVGameplayLoopTests/backups/20260924T094501Z/102.json | 05CB8DF2…AACE | 一致 | 14351/14351 | 一致 |
| 17 | CodexOutput/upserver/其他AI开发接手说明-4C2扣费前拦截-2026-09-24.md | 1E632D50…90F5 | 一致 | 7360/7360 | 一致 |
| 18 | CodexOutput/upserver/其他AI开发接手提示词-4C2-2026-09-24.md | 981CB6C5…B2AC | 一致 | 1189/1189 | 一致 |
汇总:18/18 一致(hash与字节数均一致),0项不一致。清单自述的"handoffUpdate:原16项复验一致"与本轮18项全对结果相容。
## 二、对方案末尾四个问题的逐项回答
### 问题1:现有证据是否只能定位到合并门,尚不能确定具体谓词?
结论:同意。证据只能证明命中 FishingBehaviorB.cs:49~51 的合并if,不能区分七个析取条件中的哪一项为真。
证据:
- 拦截点与消息唯一:`events.jsonl` 第11行(09:44:05.1516474Z)`kind=fault, message="Only one ordinary paid fishing action is permitted."`;该字符串在生产代码中仅出现一处——FishingBehaviorB.cs:51 的 `throw ...Stop("Only one ordinary paid fishing action is permitted.")`(全库grep确认,无其他产生点)。
- 栈帧与当前源码逐行对齐:`real-after-attempt-errors.json` 第12行栈显示 `ConsumeBaitOnKVAsync d__4:MoveNext (at FishingBehaviorB.cs:51)` ← `Stop (at UserService.KVGameplayLoop.cs:280)` ← `Fault (at UserService.KVGameplayLoop.cs:287)` ← `ConsumeBait (at FishingBehaviorB.cs:801)`。当前源码实算:51行=throw+Stop、KVGameplayLoop.cs:280=Stop定义、:287=Fault内Debug.LogError、FishingBehaviorB.cs:801=ConsumeBait的KV分支调用,全部吻合(FishingBehaviorB.cs hash已对上清单,无事后改动)。
- 早于扣费/奖励:门在49~51行,`PrepareFishingReward` 在62行、`ConsumeEnergyForKVLoopAsync` 在63行;异常在51行抛出后直达70行catch,62~68行未执行。旁证:events.jsonl中无 `fishing-reward-preflight`(仅由 UserService.KVFishingReward.cs:144 在 PrepareFishingReward 内审计)、无 `fishing-consume-confirmed`(仅由 UserService.KVGameplayLoop.cs:241 在 ConfirmFishingAsync 内审计)、09:40:42之后再无 write-attempt/accepted。
- 无法回溯谓词输入:该消息为七个条件共用,`real-after-attempt-errors.json` 与 `real-final-idle-state.json` 均只含同一句合并消息(另3条为无关的PlayFab域名解析旧错误);Fault 后 `Ready=false`(KVGameplayLoop.cs:283),`FishingCurrent` 恒false(:190-191),运行已closed(events.jsonl:12),拒绝瞬间的 fishingData 快照无处可取。
- 45键全同成立:`real-attempt-final-diff.json` 记 `changedAfterAttempt=[]`、`keyCount=45`、`paidConsumption=false`、`fishReward=false`;本审查另用 ConvertFrom-Json 独立逐值比对两份备份,data 下45键全部相同、后备份无新增键,唯一差异是顶层 `at` 时间戳(这解释了两备份字节同为14351而SHA256不同),"逐值相同"表述准确。
### 问题2:六个字段是否足够唯一解释七条件,且不需新增状态?
结论:足够。七个谓词都是这六个字段值的纯函数,读出六个值即可逐一判定七条件的真值;不需要新增状态。
证据(源码定义与对应关系):
- `fishItem==null`(条件1)⟺ `fishItem?.ID` 为null标记。fishItem 为公共字段:FishingData.cs:32。
- `IsGeneralFish()`(条件2取反)= `fishItem.SubType == 1`:FishingData.cs:172-175。即 `fishItem?.SubType==1` 与该谓词完全等价(由于 `||` 短路,此谓词仅在 fishItem 非null时才被求值)。
- `IsFree`(条件3):公共bool字段,FishingData.cs:111。
- `IsRetry`(条件4):getter `return RetryCount>0`,FishingData.cs:113-116。
- `IsDuel`(条件5):公共bool字段,FishingData.cs:112。
- `IsPackItemFish()`(条件6)= `fishItem.SubType==21 || fishItem.SubType==22`:FishingData.cs:188-191;其中21=BarginPack(:184-187)、22=PiggyBank(:180-183)。即 `fishItem?.SubType∈{21,22}` 与该谓词完全等价。
- `curFishData==null`(条件7)⟺ `curFishData?.ID` 为null标记。curFishData 为公共字段:FishingData.cs:27。
字段类型与getter无副作用(问题3一并回答):`Item.ID`、`Item.SubType` 均为 `public int { get; private set; }` 自动属性(Tables/Item.cs:54、:71);`FishData.ID` 同(Tables/FishData.cs:156);`IsFree/IsDuel` 为简单字段。所以六字段全部是只读当前对象的简单成员读取,无新增状态。
说明两点(非缺陷):(a) `fishItem?.ID` 对七条件的判定并非必需(SubType已覆盖条件2/6),但能唯一定位物品行便于后续对照表配置,保留合理;(b) 当 SubType=21/22 时条件2与条件6同时为真、IsFree等亦可与其它条件并存——诊断文字报告的是状态全集,能推出每个谓词的真值,但不标识"哪个析取项最先短路",这是合并门的固有属性,不影响定位。
### 问题3:仅改变拒绝消息是否保持时序/副作用边界,是否有null/日志异常风险?
结论:保持。存在一个必须在实现中显式处理的null文本陷阱(见P2-1),其余无风险。
证据:
- 时序:改动只替换51行throw中Stop(...)的字符串字面量为插值字符串。C#求值顺序为"先求实参、再调Stop",插值发生在谓词判定之后、Stop/Fault之前,全程同步、无await、无事件、无随机/选鱼/存档调用;49~50行谓词与throw/Stop结构不动,时序不变。
- 副作用:全部读取为自动属性/纯字段/纯getter(见问题2);`fishingData` 自身在49行已被解引用,故到51行时必非null(若为null,消息将是NRE文本而非当前消息,已被实际运行排除)。
- 日志通道:消息经 `Stop`→`Fault`(KVGameplayLoop.cs:280、281-290)单通道写入 events.jsonl(:286 Audit、:294单行JSON追加)与Console(:287 LogError)及Error属性(real-final-idle-state.json可见);70行catch再调Fault时被 `Interlocked.Exchange(ref failed,1)`(:284)去重,不会产生第二个fault事件。插值内容只有int/bool与固定"null"文本,无引号/换行,不会破坏events.jsonl的JSON单行结构;全库无任何代码按该消息文本做逻辑分支或断言匹配(既有提取测试断言只查 Faults/Calls 计数),改文字不破坏既有行为。
- null安全:`fishItem?.ID`、`fishItem?.SubType`、`curFishData?.ID` 用 `?.` 安全;`IsFree/IsRetry/IsDuel` 在非null的fishingData上直读安全。唯一陷阱:C#插值中 `int?` 为null时输出空串而非"null"(`$"{fishItem?.ID}"` 在fishItem为null时打印空),必须显式处理才能兑现方案"null明确写null"的承诺(实现例:`fishItem==null?"null":fishItem.ID.ToString()` 或 `(object)fishItem?.ID ?? "null"`)。若不处理,条件1/条件7与"空显示"将混淆,直接削弱本方案唯一目的。
### 问题4:编译及既有入口验证是否足够,是否批准这一个文件、这一处修改?
结论:验收组合足够、批准范围正确,但验收第3项需按P2-2补一步(提取测试shim同步),故整体为有条件批准。
证据:
- "静态diff只有错误文字变化":对纯文字改动是恰当且接近充分的验收;建议明确diff判据为——49~50行谓词与全文件其余部分字节级不变,仅51行字符串字面量变化(见P3-1)。
- "Editor和SERVICE_HOST编译":该分支位于 `#if UNITY_EDITOR && !SERVICE_HOST`(FishingBehaviorB.cs:27),Editor编译直接覆盖改动;SERVICE_HOST编译不含该分支,作为全局回归无害,两者都应做。
- "利用既有消费入口提取测试核对拒绝仍先于扣费":该测试真实存在且恰好覆盖此门——`KVFishingRewardTests/ConsumeControlTests.cs.txt` 第10~18行对 free/retry/duel/pack/special/missing-fish/missing-item 等逐项断言 `player.Calls==0 && Continues==0 && Faults>0`("pre-consumption gate"),即七条件每一项单独触发时扣费调用次数为0;断言不匹配消息文本,改文字不破坏断言。但注意:该测试编译的 `ConsumeControlSlice.cs.txt` 是手工提取副本,其shim `FishingData.fishItem` 声明为 `object` 且无 ID/SubType 成员(ConsumeControlTests.cs.txt:38)——改动后重新提取的slice引用 `fishItem?.ID/SubType` 将无法编译,必须同步给shim补一个带 `int ID; int SubType;` 的Item类型并重跑(属既有测试维护,不违反"不新建框架")。
- 批准范围:仅 FishingBehaviorB.cs 一处、仅原拒绝分支错误文字、不放宽准入、不新增日志点/框架/第二个Fault——与源码结构相容,可实施。
## 三、问题列表
P1(阻塞):无。
P2(应修,实施时必须处理):
1. null文本插值陷阱:`$"{fishItem?.ID}"`/`$"{curFishData?.ID}"` 在对象为null时输出空串而非"null",无法兑现方案"null明确写null"。实现必须显式判null输出字面"null"(如 `fishItem==null?"null":fishItem.ID.ToString()` 或 `(object)fishItem?.ID ?? "null"`),并在静态diff中可核对该处理存在。
2. 提取测试需同步:`ConsumeControlSlice.cs.txt` 须从改后源码重新提取,且 `ConsumeControlTests.cs.txt:38` 的shim `FishingData.fishItem`(现为 `object`)需补带 `ID/SubType` 的Item类型,否则验收第3项(既有消费入口提取测试)无法编译执行。此项属既有测试维护,不是新框架。
P3(建议):
1. 验收"静态diff只有错误文字变化"建议写成可执行判据:diff仅涉及 FishingBehaviorB.cs 第51行字符串字面量,第49~50行谓词与文件其余部分字节级不变,无其他文件变更。
2. 在方案或实施记录中注明诊断语义:六字段报告的是状态全集,可推出七条件各自真值,但不标识"哪个析取项最先短路"(SubType=21/22时条件2与6必然同真),避免后续读者误期待单一原因。
3. 精确性备注:该门位于第48行 `ClaimFishingConsumption`(内存标记Consuming=true,非扣费、无审计事件)之后;"扣费前拦截"表述成立,但如后续文档复述顺序时可注明这一内存claim先于门发生。
4. 两份102备份SHA256不同但45键逐值全同(仅顶层 `at` 时间戳不同、字节数相同),建议在证据文档注明,避免后人把hash差异误读为数据变化。
## 四、最终结论
有条件批准实施。条件(全部满足后方可编码合入):
1. 仅修改 `Client/Assets/Scripts/GampPlay/FishingB/FishingBehaviorB.cs` 第51行Stop(...)的字符串实参:保留第49~50行合并if谓词与throw/Stop调用结构字节级不变;错误文字含六字段(fishItem?.ID、fishItem?.SubType、curFishData?.ID、IsFree、IsRetry、IsDuel),null显式输出"null"(按P2-1方式实现,禁止裸 `?.`+插值依赖)。
2. 不新增任何日志点/框架/文件/await/事件/第二个Fault;成功分支与其余门(offline/activity/bad-mag/context-change)文字不变。
3. 验收按方案四项执行,其中提取测试按P2-2同步 `ConsumeControlSlice.cs.txt` 与shim并重跑通过(10类前置门断言含 `player.Calls==0`);静态diff按P3-1判据核对;Editor与SERVICE_HOST双编译通过。
4. 编码完成后由主会话核hash、逐条裁决,再决定后续运行;本诊断不构成根因修复、不构成4C2验收、更不授权新的真实运行。
## 五、边界声明
- 本审查基于2026-09-24冻结的hash清单(18/18一致)与当刻工作区状态,只做只读核对;未运行Unity、未连接正式服务器或测试账号、未执行任何写操作类命令(Get-FileHash/ConvertFrom-Json/diff/sha256sum/wc均为只读)。
- 本审查不等于4C2关闭:4C2实施复核、真实收鱼奖励闭环、新进程恢复均未完成;4C2/完整4C/G4维持未关闭。
- 本审查不构成对任何新的真实运行的授权;新的真实运行仍需按记录要求先核对新进程、备份与实际操作范围,并向用户明确一次尝试。