24 KiB
客户端迁移详细修改方案 v1
2026-09-16执行顺序更新:按用户要求优先确认PlayFab存档到KV的读写适配,当前阶段顺序与验收门以TODO.md为准。本文A/B/C保留作技术范围分组。KV联调所需的最小测试登录属于测试准备,不要求先重构完整游戏登录。
日期:2026-09-15。状态:技术评审完成,待用户确认本文的开发边界与行为裁决。
本文是当前修改方案入口,覆盖此前草案中被新 KV 实现替代的结论。本文不代表代码已实现或正式联调已通过。
1. 基线、目标与交付边界
- 客户端:C:/projects/ft,feat/upserver,f40a816cc;工作区已有 RTService 的 SkipNegotiation + WebSockets 修改。
- 服务端:C:/projects/N3-Server-CN,main,858fe7c;KV 功能提交 a3d3faa。
- 正式 API:http://s101.local:18080/;Hub:http://s101.local:18080/gamehub。
- Unity:2022.3.62f2c1;沿用现有 HttpClient、Newtonsoft.Json、官方 .NET SignalR 客户端。
- 身份唯一来源:涂游 SDK 数字 userId,传输统一为十进制字符串。禁止把旧 PlayFab hex ID 转成数字后当作涂游 ID。
交付划分
| 批次 | 可审查交付 | 完成定义 |
|---|---|---|
| A | 正式地址、独立 KV 客户端、缓存与同步模块 | 配置与契约测试通过,KV 模块不依赖 PlayFab |
| B | 涂游登录、KV 初始化、会话和实时连接接入 | 专用账号可完成登录、恢复本人存档、写后读和业务消息验证 |
| C | 好友/资料/排行/公告/匹配等外围迁移,移除 SDK | 所有保留业务能力有替代实现,Unity/热更新编译与相关回归通过,包和程序集不再依赖 SDK |
B 必须先关闭第 10 节列出的登录资料来源问题;C 不包含在“仅修改 KV 接口”的放行范围内。当前可完成 A 的具体实现;B/C 的条件不满足时不通过删除功能、空响应或临时生成假身份绕过。
2. 已核实的接口契约
2.1 登录
POST /account/Login,JSON 请求:
{"playerId":"10006464"}
playerProfile 是可选字段。首次读取存档前不上传本地默认等级/昵称/头像,避免 AccountService 将默认值写回已有玩家资料。
成功响应字段:state=0、internalId、createTime、svrTime、newCreate;成功时签发 Cookie。HTTP 200 不代表 state 成功。客户端接收专用 DTO,字段显式映射;日期按 UTC 解析。缺少必需字段、失败 state、非法 JSON 都不能进入游戏。
2.2 KV
全部为 POST、JSON、Cookie 鉴权。用户 ID 取 NameIdentifier,不放在 KV 请求 body 中。
| 路由 | 请求示例 | 成功响应 | 行为 |
|---|---|---|---|
| playerdata/Get | {} | {"data":{"Lv":"12"}} | 读取当前账号全部键值 |
| playerdata/Get | {"keys":["Lv","NewPlayerDataDic"]} | {"data":{...}} | 缺失 key 省略 |
| playerdata/Set | {"data":{"Lv":"13"}} | 200,空正文 | 新增/覆盖提交的键,保留其他键 |
| playerdata/Delete | {"keys":["Lv"]} | 200,空正文 | 删除指定键,缺失键无操作 |
- Get 的 keys 省略/null/空列表均读全部;Delete 的空列表无操作;Set 的空字典无操作。
- Get 成功必须有非 null 的 data 对象。空正文、data 缺失或格式错误不是空存档。
- V 是字符串;JSON 对象由客户端先序列化成字符串,再放进 data。不能把整个存档对象直接替换成 HTTP data 对象的内部字段。
- MySQL PlayerKV:主键 (PlayerId,K),K 最大128字符,V 为 longtext,UpdateTime 由服务端写入。
- Get 不返回 UpdateTime 或版本。当前服务端无条件更新,没有条件版本写入/幂等请求号,不能承诺跨设备并发安全。
- Set/Delete 以 HTTP 成功判定,不能用响应内容是否为空判定。
2.3 SignalR
保留 CookieContainer,WebSockets-only,SkipNegotiation=true,仍执行 SignalR 协议握手。业务登录用的 HttpClient 与 Hub 必须共享当前会话 Cookie,旧账号会话不能被新账号复用。
3. 模块与文件修改清单
以下是计划路径,尚未创建的源文件用“新增”注明;实施时不修改服务器源码。
| 文件(相对 Client) | 改动职责 |
|---|---|
| Assets/Scripts/AOTScripts/GConstant.cs | 共用正式 API 默认常量 |
| Assets/Editor/DebugConfig.cs | 更新 s101 API 常量;菜单校验同时核对 API 字段 |
| Assets/Scripts/Core/CustomServerMgr.cs | 同一基址、显式 HTTP 结果、会话捕获/失效、请求取消;保留旧业务方法签名以控制影响面 |
| Assets/Scripts/Services/PlayerData/PlayerKVModels.cs(新增) | Get/Set/Delete DTO、业务错误种类、同步状态 |
| Assets/Scripts/Services/PlayerData/PlayerKVClient.cs(新增) | 远端请求、严格响应解析、空响应成功判定;无 Unity 生命周期逻辑 |
| Assets/Scripts/Services/PlayerData/PlayerKVSyncState.cs(新增) | 账号缓存、待写队列、序号、批次确认与三方比较;不依赖 MonoBehaviour |
| Assets/Scripts/Services/PlayerData/PlayerKVCacheStore.cs(新增) | 按环境/账号隔离保存快照与待同步记录、加载校验、备份及写入失败报告 |
| Assets/Scripts/Core/PlayFabMgr.cs | B批切换时保留 MonoBehaviour 类名和 .meta GUID,作为业务兼容入口;内部委托新 KV 模块,同批替换登录调用的 UserDataRecord 签名。A批不激活替换,避免旧登录流程调用未初始化的新KV |
| Assets/Scripts/Services/UserService.cs | 新登录 DTO、await 授权/KV 初始化、身份/时间/状态管理;PlayFab 调用按批次拆除 |
| Assets/Scripts/Services/AggUserService.cs | loginInfo.StrUserId 接入,成功/失败进度、绑定信息与资料初始化顺序 |
| Assets/Scripts/Stages/PFLoginAct.cs、LoginStage.cs、LoginStageCtx.cs | 明确涂游入口/编辑器入口,移除把 PlayFab Title 当初始化依据的条件;防止非 AGG 分支漏改 |
| Assets/Scripts/Services/RTService.cs | 已有直连选项;补首次连接注册、释放订阅和换号会话隔离 |
| Assets/Scripts/SubBoostrap.cs | 注册 KV 依赖,校验最终配置在服务构造前生效 |
| Assets/Scripts/GampPlay/FishingB/FishingBehaviorB.cs | 立即同步失败后玩法状态/重试语义,不能仅日志忽略 |
| Assets/Scripts/UI/Turntable/FishingTurntablePanel.cs | 仅成功时设置 isDataSynced;重试同一存档值,不重复扣费 |
保留 PlayFabMgr 类名是为了避免所有场景和玩法引用一起重命名;这不代表继续依赖 SDK。最终包依赖检查与类名是否包含 PlayFab 分别判断。新代码使用业务命名。
4. 配置和身份规则
- API 与 Hub 均从 EventApiURL 解析,缺省用共用常量;URL 必须为绝对 HTTP(S),规范末尾斜杠后组合相对路由。
- 远端配置先加载、本地 debugconfig.json 后覆盖。实施需定位实际 persistentDataPath,仅改 EventApiURL;不得通过整套调试菜单顺带覆盖静态资源、RTM 和 Func 字段。
- 云配置发布不在客户端代码批次内。包上配置若仍提供旧值,默认常量不会覆盖它;验收需检查实际最终值。
- 发布客户端必须由涂游 SDK 登录获取 ID;ID 缺失或非法立即失败,不回退设备 ID、PlayFab ID 或随机 ID。
- 编辑器允许显式输入分配给联调的十进制测试 ID,不能把短数字默认拼接成真实用户,也不能把任意测试 ID 当作已完成渠道认证。
- 在字符串层保留 ID;需要验证范围时使用 long.TryParse + InvariantCulture。当前涂游包装字段为 int,不能通过自行改类型声称上游已支持 long。
- 清理三个已定位 hex 转换点:FriendService 邀请码、SurveyService 用户尾号、DinggingSceneData 随机种子;修改必须连同编码/解码及玩法可复现测试评审。
- 历史 JSON 字段 PlayFabId/FriendPlayFabId 仍按服务端现有契约使用,值是涂游 ID,不能单端改字段名。
5. 登录和退出时序
登录状态
Idle → ChannelAuthenticated → ApiAuthenticated → LoadingKV → Initializing → Ready。 任一步失败进入 Failed;存档冲突进入 Conflict;退出/换号增加 sessionGeneration,失效所有旧请求回调。
- 确认最终配置,初始化新的会话容器。
- 涂游登录成功,验证数字 ID;已有 SDK 登录态取 TYAccountInfo.userId。
- await account/Login;检查 HTTP、DTO、state;暂存身份及 Cookie。
- 用 svrTime 校时,用 createTime/newCreate 初始化账号生命周期标志。
- await playerdata/Get({}),将云端字典与当前账号本地记录按第6节比较。失败不 LoadPlayerData、不启动写入、不返回 Done。
- 缓存模块先就绪,允许 LoadPlayerData 与 Agg InitData 中现有同步调用记录变更;此时网络同步仍暂停。
- 完成必要资料来源处理及 LoadPlayerData、OnInitData,初始化成功后启动同步循环。
- 准备好聊天/邮件/实时订阅后发布现有 EvtAPISvrLoginSuccess,避免这些消费者先于存档初始化读取状态。内部 ApiAuthenticated 状态不通过旧公共事件提前广播。
- Login 返回 Done;SignalR 若连接失败独立报告实时连接状态。完整联调必须再确认其连接与业务收发,不能以 Done 代替实时成功。
重登和退出
- 同一会话多个401/403合并为一次重登,登录请求本身不触发递归重登。
- KV请求捕获发起时的 ID、会话 generation、Cookie 所属 HttpClient;旧请求不能在排队后使用新账号客户端发送。
- 退出先停止接收业务写入并保存待同步记录,再停止/释放 Hub 和网络请求;不依赖应用退出时网络请求一定完成。
- 换号创建新 CookieContainer;旧回调不得确认或修改新账号队列。
- 已发出的旧请求即使取消,也可能在服务器提交;取消只表示客户端停止等待,不能解释成服务端未写入。
6. 没有服务器时间戳时的存档方案
6.1 路线评审
| 方案 | 优点 | 问题 | 裁决 |
|---|---|---|---|
| 云端全量覆盖本地 | 简单,适合干净首次登录 | 离线或失败待写修改丢失 | 仅用于没有待写记录时 |
| 本地时间较新就覆盖云端 | 接近旧实现 | 新API无远端时间,客户端时钟也不可靠 | 不采用 |
| 云端基线 + 待写日志 + 值的三方比较 | 保留失败修改,检测可见冲突,不需修改服务器 | 无法提供服务器版本锁、跨设备强一致性 | 推荐本期有限保证方案 |
| 服务端新增revision/条件写/幂等号 | 可解决并发覆盖和超时确认问题 | 超出已有API,需双方另行实现 | 多设备强一致需求的前置工作 |
推荐方案仅保证本客户端正常顺序发送的队列正确性;不承诺多设备并发或超时请求延迟提交下绝不回退。若本期要求后两者,裁决改为先补服务端契约,不能仅靠客户端重试满足。
6.2 本地持久化
使用版本化缓存文件,路径包含环境指纹和涂游 ID,例如 persistentDataPath/player-kv/v1//.json;originHash 由规范化 API origin 计算,避免主机特殊字符与跨服混用。
单份快照包含:formatVersion、环境、playerId、最后确认的云端键值、pending 修改及其本地序号/base存在标记/baseValue、本地专用值。每个键最多一个待写“最新目标值”,正在发送的批次使用独立不可变快照。
- 文件写入临时文件后替换,保留上个完整备份;加载校验归属与格式。主文件损坏读备份;都损坏则报告恢复失败并保留原文件,不能静默当新账号。
- 修改后尽快保存完整记录;更新方法仍为同步入口,优先保证待写记录持久化后再安排网络写入。写入失败进入 StorageError,立即同步返回失败,不能声称已可靠保存。
- 不复制 LocalData_<旧PlayFabId>/LastUpdateTime_<旧PlayFabId>。即使旧 ID 恰好只含数字,也不能认定与涂游 ID 同源。
- remoteStore=false 的值保存在 localOnly 区,仅本人本环境使用,不加入远端队列,避免和远端确认基线混为一谈。
6.3 每键比较规则
首次登录无本地pending:云端为基线,成功空字典表示服务器当前无键。业务默认值仅在存档模块初始化后按现有玩法规则生成。
有本地pending时,取 B=编辑前最后确认的云端值,L=本地待写值,R=本次Get值,存在/不存在单独记录;空字符串不等于不存在。
| 条件 | 处理 |
|---|---|
| R == L | 云端已具备目标值,确认该pending;适用于上次响应丢失后的常见恢复 |
| R == B | 未观察到云端变化,保留L并允许按队列提交 |
| R与B、L均不同 | 标记Conflict,保留三份数据,不自动覆盖任何一侧 |
| 没有可靠B | 除R==L可确认外均标记Conflict,不猜测先后 |
Conflict 状态不开放玩法写入,用现有错误/重试入口提示存档存在冲突;重试不自动丢弃本地记录。日志只记键名、错误类别和摘要,不输出完整存档。人工数据恢复工具不包含在本批。
字典值中的JSON采用字符串精确比较;序列化格式差异可能产生保守冲突,禁止为了减少冲突把未知业务JSON合并。
7. 写入队列与立即同步
正常批量
- 保留40秒周期作为常规同步触发;每批最多10个key属于客户端节流策略,不宣称是新服务器限制。
- 取消旧5个缓冲桶最多50个不同key的限制;新的待写字典不能静默丢弃第51个key。
- 同一会话只允许一个KV变更HTTP请求正在等待;普通Set、立即Set、Delete共用排队入口。
- 发送时复制 key/value/sequence,不将后续可修改字典直接交给请求。
- 成功确认只移除当前pending中sequence与批次一致的项;若发送v1期间产生v2,v1确认后v2继续待写,并更新其base为已确认v1。
- 失败保留目标值和基线,不把失败项伪装为已同步。
失败分类
| 情况 | 动作 |
|---|---|
| 未认证401/403 | 暂停写入、合并重登;恢复时重新读取并比较 |
| 400及明确无效请求 | 停止该项自动重试,报告key/类型;保留原始记录 |
| 连接错误/超时/500 | 写入结果可能未知,冻结后续写并进入重读恢复流程;不得立即发送反向扣款/回滚 |
| Get也失败 | 保留本地状态并等待重连,不默认空云端 |
| 缓存持久化失败 | StorageError,阻止新的需可靠存档的操作 |
恢复重读可以发现很多“已提交但响应丢失”的情况,但不能排除旧请求稍后提交,也不能提供服务器事务序号保证。并发及超时严格一致性仍受第6.1节边界约束。
SyncDataImmidiate 兼容语义
保留现有签名 Task SyncDataImmidiate(string key,string data),内部以同一队列立即调度;true只在该请求目标已获远端成功确认时返回。失败或结果不确定返回false,同时保留状态和待恢复记录。
调用者不能在超时后重新执行一次“减余额”生成新值,应重试已捕获的目标存档。false不等于服务端未执行。
- 捕鱼:原代码先本地扣体力再发请求;需要冻结该操作的重复触发,在未确认时走恢复/重试,禁止简单补回再重复扣导致双写。该玩法状态处理与KV入口一起验收。
- 转盘:仅true时设置isDataSynced;false走重试/恢复,不因返回了布尔值就解锁结果。
- KV存储不是扣款事务接口,不能承诺支付或扣道具的exactly-once语义;如玩法需要此保证,应由服务器业务接口提供操作幂等性。
删除
PlayerKVClient 提供 DeleteAsync 以覆盖现有服务器契约,写入规则同样按key顺序串行;删除意图用显式标记,不将null或空字符串当作删除。当前无业务删除调用时不新增玩法入口;删除恢复也用存在标记做三方比较。
8. HTTP 层与接口建议
新增结构化返回:Success、StatusCode(无HTTP响应时为空)、ErrorKind、Body,不改变已有CustomServerPost的string/null返回契约以避免扩大影响。
// 方案接口,尚未实施;所有请求绑定调用时的会话。
Task<KVGetResult> GetAsync(IReadOnlyList<string> keys, CancellationToken token);
Task<KVWriteResult> SetAsync(IReadOnlyDictionary<string, string> data, CancellationToken token);
Task<KVWriteResult> DeleteAsync(IReadOnlyList<string> keys, CancellationToken token);
KVGetResult = 成功标志 + Dictionary<string,string> Data + HTTP/错误信息;KVWriteResult = 成功标志 + HTTP/错误信息。Get解析失败必须失败,Set/Delete不解析空正文。所有例外映射到取消、网络、HTTP、协议四类,不吞异常返回空存档。
沿用15秒单次请求超时;恢复重试使用有上限的退避(2/5/10/30秒),仅在会话有效且无Conflict/StorageError时继续。退出取消所有后台循环,Task.Delay带取消令牌。
9. SignalR 配套修正
- 保留已修改的两个直连选项及Cookie。
- 在每次新HubConnection创建后,先注册全部已有业务处理器,再StartAsync;不把首次注册仅挂在Reconnected。
- 同一HubConnection重连保留已有处理器,不重复注册;新连接则从持久订阅表重建。
- WaitForAPISvrLogin保存/释放订阅,重复调用不重复建连;Release等待停止/释放或由会话协调器观察其完成。
- 会话更新先结束旧Hub,再绑定新Cookie重建;旧连接回调按generation过滤。
- 用服务器已有Test(arg1,arg2)返回OK验证请求/响应;推送验证使用测试环境已有消息,不调用生产强制下线管理接口。
10. 登录资料与SDK彻底移除的剩余裁决
新增KV解决存档承接,但不自动生成旧PlayFab提供的所有字段。
| 能力 | 当前事实 | 本方案裁决 |
|---|---|---|
| 本人昵称/头像 | 登录不返回;SearchFriend可返回基础PlayerInfo,但缓存失败可能返回Unknown默认数据 | 可作为查询候选,禁止把默认值当可靠资料再回写;B批需确定权威来源或扩展登录资料响应 |
| GetHomePage | 返回好友邀请/推荐列表 | 明确排除为本人资料恢复接口,修正旧文档候选建议 |
| 上次登录时间 | LoginResp不返回 | 优先服务端返回登录前时间;若采用新KV键记录需明确首次缺失策略,不将当前时间冒充历史时间 |
| 地区 | LoginResp和KV均不自动提供 | 地区榜适配前确定来源,不默认伪造国家/大洲 |
| 渠道绑定 | SDK与旧KV均有标志 | 渠道SDK为账号绑定权威;旧KV标志不得自行升级认证状态 |
| 好友 | 有GetFriendsList,Friends是JSON字符串数组 | 可迁移;Level直接映射,LastLogin缺失需单独展示裁决 |
| 他人KV | 新KV仅限本人 | GetPlayLv等改用资料查询;不向私有KV接口增加任意playerId |
| 普通/地区等级榜 | 当前服务端有活动榜 | 不能直接互换,需要等价契约或明确产品范围 |
| 公告 | 未确认等价接口 | C批条件未满足 |
| PlayFab匹配票据 | DuelRT不是票据服务 | C批条件未满足 |
| EntityKey | 新登录不提供 | 移除其真实消费者后才能删除;不能构造伪EntityKey骗过编译 |
因此SDK包删除必须最后执行。阶段内SDK包暂留不代表整体目标已完成,也不允许正式进入仍按旧身份调用PlayFab的功能。
11. 验证矩阵
新增纯逻辑测试放入 Client/Assets/Tests/PlayerData/(新Editor测试程序集;引用按现有HotUpdate与Unity Test Framework定义),测试核心状态机使用内存远端/缓存替身;测试不用硬编码正式账号。
| 编号 | 场景 | 明确预期 |
|---|---|---|
| L01 | 涂游ID缺失/非数字/超范围 | 不发业务登录,不调用PlayFab回退 |
| L02 | HTTP200但state失败或字段缺失 | 不发布登录成功,不加载KV/玩法 |
| L03 | 登录成功、KV失败 | 不返回Done,不生成空存档写回 |
| L04 | KV成功、初始化失败 | 不启动网络写队列/登录成功事件 |
| K01 | Get {} 返回data={} | 成功空存档,与网络失败区分 |
| K02 | 指定keys含缺失项 | 只返回已有项;不构造值为null的伪记录 |
| K03 | Set/Delete 200空正文 | 成功;其他HTTP错误均保留待写 |
| K04 | 发送v1时编辑v2 | v1确认不删除v2;最终再次提交v2 |
| K05 | 51个以上不同key | 全部记录并可分批同步,无静默丢失 |
| K06 | 请求已提交但响应丢失,重登R=L | 恢复确认,无二次扣减操作 |
| K07 | R=B与R不同于B/L | 前者保留待写;后者Conflict且不写云端 |
| K08 | 空值、缺失、删除 | 三种状态区分,删除缺失键幂等 |
| K09 | remoteStore=false | 保留本地专用值,不上传 |
| K10 | 本地文件损坏/磁盘写失败 | 备份恢复或StorageError,不重置存档 |
| K11 | A账号/服切到B,A请求晚返回 | B缓存/队列/会话不受影响 |
| K12 | 暂停/恢复/退出 | 单同步循环、待写已保存,不依赖退出HTTP完成 |
| G01 | 转盘立即写失败 | 不设置isDataSynced,不重复生成扣减 |
| G02 | 捕鱼扣体力结果未知 | 进入恢复,不补回后重扣 |
| S01 | 连接前已订阅事件,首次连接收到推送 | 首次即可处理,重连无重复回调 |
| S02 | WebSocket直连及重连 | 无negotiate请求,Cookie鉴权、Test返回OK |
正式联调用已分配测试账号:写独立测试key → Get核对 → 清除该账号本地测试缓存影响并重新登录读取 → Delete测试key → 再读确认不存在。只清理本次创建的测试key,不清空真实玩家存档。记录客户端版本、目标地址、部署版本、迁移状态、HTTP状态与脱敏结果。
测试分层:纯逻辑测试通过 → Unity脚本编译 → 测试环境HTTP+MySQL持久化 → 正式部署专用账号验证。服务端现有7个InMemory测试只能证明其覆盖的单进程语义;数据库主键/事务、HTTP授权和部署迁移需实际验证。
12. 执行任务与验收门
- A1:配置入口修改,验证最终API/Hub基址,保留其他服务地址。
- A2:DTO/HTTP/KV客户端实现,覆盖Get解析、200空响应、授权与超时分类测试。
- A3:纯KV状态与持久化实现,先覆盖K04~K12失败用例,再接MonoBehaviour。
- A4:审核所有CompareDatas/立即同步/remoteStore=false调用,以测试宿主验证新KV入口;保留当前生产登录及PlayFabMgr调用链,防止半切换破坏已有初始化。
- B1:关闭第10节资料来源条件,完成身份/登录阶段/SDK回调具体映射的补充评审。
- B2:同一批次切换PlayFabMgr内部与登录调用签名,按第5节接入登录与会话、按第9节修正实时订阅,运行L/S用例。
- B3:审核捕鱼与转盘完整交互路径,运行G用例,验证恢复不会重复消费。
- B4:专用账号KV与实时端到端验收,输出脱敏记录。
- C1:为剩余SDK直调功能确定替代接口与产品裁决。
- C2:移除所有运行/编译依赖,清理manifest、lock、asmdef GUID、StripHint、link和热更新输入;Unity重新生成派生文件。
- C3:执行相关玩法回归和热更新构建验证,全部通过后才能宣称SDK移除完成。
13. 评审裁决与待确认项
| 评审项 | 裁决 | 依据 |
|---|---|---|
| 正式地址/涂游身份/三项KV路由/Cookie | 已核实 | 用户确认及服务端源码 |
| 空响应成功、全量读空keys、本人隔离 | 通过 | DTO/Controller/Service实现一致 |
| 兼容业务入口、独立KV模块、失败不丢队列 | 通过 | 控制影响面并覆盖现有可靠性缺口 |
| 无时间戳缓存恢复 | 条件通过 | 用户接受第6节有限保证与冲突阻止写入;不是跨设备强一致 |
| A批开发边界 | 技术评审通过,待用户确认 | 本文给出职责、契约、失败处理和验收;不要求改服务端 |
| B批完整游戏登录 | 条件未关闭 | 昵称/头像、历史时间、地区与仍存SDK消费者需明确处理 |
| C批彻底删除SDK | 暂不放行 | 公告/等级榜/匹配等尚无完整替代裁决 |
| 执行结果 | 未执行 | 本次仅文档,已有SignalR选项修改保持不变 |
建议本次确认:先实施A批;采用第6节“云端基线+待写日志+冲突阻止自动覆盖”,接受现有接口有限并发保证。若要求多设备/超时条件下严格防覆盖,则先增加服务端版本条件写入,再放行存档模块。