150 lines
6.6 KiB
Markdown
150 lines
6.6 KiB
Markdown
## ADDED Requirements
|
||
|
||
### Requirement: MongoDB 数据库连接组件
|
||
|
||
服务端 MUST 在 Gate Scene 启动时初始化 `DBComponent`,封装 MongoDB.Driver 提供异步读写。连接配置从 `StartZoneConfig.DBConnection` 和 `DBName` 读取。
|
||
|
||
#### Scenario: 启动时建立连接
|
||
- **WHEN** ET Server Gate Scene 启动
|
||
- **THEN** `DBComponent` 从 StartZoneConfig 读取 mongodb 连接字符串
|
||
- **AND** 创建 `IMongoClient` 实例
|
||
- **AND** 测试连接成功(PING)
|
||
- **AND** 失败时记录错误日志并按 StartConfig 决定是否阻塞启动
|
||
|
||
#### Scenario: 集合自动创建
|
||
- **WHEN** 首次写入某类型 Entity(如 Player)
|
||
- **THEN** `DBComponent` 自动创建对应 collection(命名 `players`、`heroes`、`bags`...)
|
||
- **AND** 创建索引(默认按 EntityId)
|
||
|
||
### Requirement: 玩家数据加载
|
||
|
||
玩家登录 Gate Scene 成功后,服务端 MUST 从 MongoDB 加载该玩家的 `Player` Entity(含 `HeroComponent` + `BagComponent`)。若玩家首次登录,MUST 创建新 Player 并发放初始资源(由 `InitialResourceConfig` 配置)。
|
||
|
||
#### Scenario: 老玩家登录
|
||
- **WHEN** 玩家 PlayerId=10001 通过 `C2G_LoginGate` 登录
|
||
- **AND** DB 中存在记录
|
||
- **THEN** 服务端从 DB 加载完整 Player Entity 到内存
|
||
- **AND** Player.GetComponent<HeroComponent>() 包含所有英雄子 Entity
|
||
- **AND** Player.GetComponent<BagComponent>() 包含所有道具
|
||
|
||
#### Scenario: 新玩家首次登录
|
||
- **WHEN** 玩家 PlayerId 在 DB 中不存在
|
||
- **THEN** 服务端创建 Player Entity
|
||
- **AND** 添加 HeroComponent + BagComponent
|
||
- **AND** 按 `InitialResourceConfig` 发放初始道具/英雄
|
||
- **AND** 立即写入 DB(同步保证幂等)
|
||
|
||
### Requirement: 数据持久化策略
|
||
|
||
服务端 MUST 采用 **脏标记 + 定时刷盘** 策略,避免每次业务操作直写 DB:
|
||
- 业务系统通过 `EntitySystem.SetDirty(entity)` 标记脏数据
|
||
- `DBSaveComponentSystem` 每隔 N 秒(默认 30s)扫描所有脏 Entity 并批量落库
|
||
- 玩家离线(session disconnected)时 MUST 立即强制刷盘
|
||
|
||
#### Scenario: 业务操作触发脏标记
|
||
- **WHEN** `C2G_SummonHeroHandler` 修改 HeroComponent
|
||
- **THEN** 通过 `Player.SetDirty()` 标记
|
||
- **AND** Handler 立即返回(不等落库)
|
||
|
||
#### Scenario: 定时批量刷盘
|
||
- **WHEN** DBSaveComponent 定时器触发
|
||
- **THEN** 扫描所有标记 dirty 的 Player
|
||
- **AND** 异步 `DBComponent.Save(player)` 批量写入
|
||
- **AND** 写入成功后清除 dirty 标记
|
||
- **AND** 失败时保留 dirty 等下次重试
|
||
|
||
#### Scenario: 玩家离线立即刷盘
|
||
- **WHEN** Gate Scene 收到 SessionDispose 事件
|
||
- **AND** 该 Session 对应的 Player 是 dirty
|
||
- **THEN** 同步等待 `DBComponent.Save(player).Coroutine()` 完成
|
||
- **AND** 再释放 Player Entity
|
||
|
||
### Requirement: Entity → BSON 序列化
|
||
|
||
`DBComponent` MUST 使用 ET 的 MongoHelper(基于 MongoDB.Bson)序列化/反序列化 Entity。所有要持久化的 Component 字段 MUST 加 `[MongoElement]` 特性,子 Entity 通过 ET 的 Entity 关系自动遍历。
|
||
|
||
#### Scenario: 持久化 HeroComponent
|
||
- **WHEN** `DBComponent.Save(player)` 触发
|
||
- **THEN** 序列化 Player 的所有 Component 包含 HeroComponent + BagComponent
|
||
- **AND** 子 Hero Entity 递归序列化
|
||
- **AND** 写入 MongoDB `players` collection(按 PlayerId 为 \_id)
|
||
|
||
#### Scenario: 反序列化恢复
|
||
- **WHEN** `DBComponent.Query<Player>(playerId)` 触发
|
||
- **THEN** 从 BSON 重建 Player Entity 树
|
||
- **AND** 所有子 Entity(Hero、Item 等)的父子关系正确恢复
|
||
- **AND** 通过 `Player.GetComponent<HeroComponent>().GetHero(heroId)` 能直接访问
|
||
|
||
### Requirement: 并发与一致性
|
||
|
||
同一玩家的业务请求 MUST 在 Gate Scene 串行执行(ET 单线程纤程天然保证)。但跨玩家操作(如交易、组队)MUST 通过 ET 的消息机制传递,避免直接跨纤程访问。
|
||
|
||
#### Scenario: 同玩家串行请求
|
||
- **WHEN** 客户端在 1 帧内连续发送 SummonHero 和 HeroAddExp
|
||
- **THEN** Gate Scene 单线程依次处理两个请求
|
||
- **AND** 第二个请求看到第一个的副作用(新英雄存在)
|
||
|
||
#### Scenario: 跨玩家通过消息
|
||
- **WHEN** 玩家 A 给玩家 B 赠送道具(未来需求)
|
||
- **THEN** 必须通过 ET 的 Actor 消息机制
|
||
- **AND** 不允许 A 的纤程直接修改 B 的 BagComponent
|
||
|
||
### Requirement: DB 索引
|
||
|
||
服务端 MUST 为常用查询字段建索引:
|
||
- `players._id` (默认按 PlayerId)
|
||
- `players.account`(玩家账号反查)
|
||
- 业务表如 `friendships`(未来需求)需自定义索引
|
||
|
||
#### Scenario: 索引创建
|
||
- **WHEN** DBComponent 首次连接 DB
|
||
- **THEN** 检查并创建 `players` collection 上的必要索引
|
||
- **AND** 通过 `IMongoCollection<Player>.Indexes.CreateOneAsync` 创建
|
||
|
||
### Requirement: 数据迁移版本号
|
||
|
||
每个持久化 Entity MUST 包含 `dataVersion (int32)` 字段,用于未来数据结构变更时支持渐进式迁移。
|
||
|
||
#### Scenario: 版本号读取
|
||
- **WHEN** 加载 Player 时 dataVersion < 当前版本
|
||
- **THEN** `DBMigrationComponent` 运行该版本到当前版本的迁移函数
|
||
- **AND** 迁移后更新 dataVersion = 当前版本
|
||
- **AND** 立即保存到 DB
|
||
|
||
#### Scenario: 新建 Player 写入版本号
|
||
- **WHEN** 创建新 Player Entity
|
||
- **THEN** 自动设置 `dataVersion = 当前最新版本`
|
||
|
||
### Requirement: 日志与监控
|
||
|
||
DB 操作 MUST 记录以下日志(基于 ET 的 Log 系统):
|
||
- 慢查询:超过 100ms 的读写
|
||
- 失败:连接断开、超时、序列化错误
|
||
- 容量:每隔 1 小时记录 collection 文档数和大小
|
||
|
||
#### Scenario: 慢查询日志
|
||
- **WHEN** `DBComponent.Save(player)` 耗时超过 100ms
|
||
- **THEN** 输出 `Log.Warning` 包含 PlayerId、耗时
|
||
- **AND** 不影响业务流程
|
||
|
||
#### Scenario: 失败重试
|
||
- **WHEN** DB 写入抛 `MongoConnectionException`
|
||
- **THEN** 框架 MUST 重试最多 3 次(指数退避)
|
||
- **AND** 仍失败则保留 dirty 标记等下次刷盘
|
||
- **AND** 输出 `Log.Error`
|
||
|
||
### Requirement: 开发模式无 DB 选项
|
||
|
||
系统 MUST 支持开发期免 MongoDB 模式:当 `StartZoneConfig.DBConnection == "memory"` 时,服务端 MUST 启用 `MemoryDBComponent`,所有"持久化"只在进程内存中(Dictionary),重启清空。生产环境 MUST 禁止使用此模式(启动时 WARNING 日志提示)。
|
||
|
||
#### Scenario: 内存模式启动
|
||
- **WHEN** StartZoneConfig.DBConnection == "memory"
|
||
- **THEN** DBComponent 实例化为 `MemoryDBComponent`
|
||
- **AND** 所有读写在 `Dictionary<long, Player>` 内存中完成
|
||
- **AND** 不需要本地起 mongod
|
||
|
||
#### Scenario: 内存模式重启
|
||
- **WHEN** 内存模式下 ET Server 重启
|
||
- **THEN** 所有玩家数据丢失(仅开发期使用)
|
||
- **AND** 日志输出 WARNING 提示此模式不可用于生产
|