diff --git a/docs/dev/item/item-store.md b/docs/dev/item/item-store.md new file mode 100644 index 0000000..52e0c6e --- /dev/null +++ b/docs/dev/item/item-store.md @@ -0,0 +1,101 @@ +# 需求综述 + +参考 `ITileStore` 的设计,为道具系统定义 Layer 0 的原始数据存储层,包含 `IItemRawData`、`IItemLegacyConverter`、`IItemStore` 接口及 `ItemStore` 实现类,并挂载至 `IDataCommon`。道具本身是一类 tile 的具体属性,通过 tile 即可定位,不需要独立的 id↔num 双向索引。 + +# 接口设计分析 + +## ItemCategory 枚举 + +在 `store/types.ts` 中与 `TileType` 并列定义,用于标识道具分类: + +- `Unknown`:未知或尚未归类的道具。 +- `Constant`:永久道具,获得后常驻生效。 +- `Consumable`:一次性道具,使用后消耗。 +- `Pick`:即捡即用道具,捡到立刻生效。 +- `Equipment`:装备类道具,可装备 / 卸下。 + +### 设计意图 + +放在 `store/types.ts` 而非 `data-common/src/types.ts`,以避免 `types.ts` ↔ `store/types.ts` 之间的循环引用。`types.ts` 通过 `./store` 导入 `IItemStore` 挂载到 `IDataCommon`;若将 `ItemCategory` 放在 `types.ts`,`store/types.ts` 引用它时会产生循环。 + +## IItemRawData + +### 设计意图 + +参考 `ITileRawData` 的设计,`IItemRawData` 是道具的原始数据定义,存储于 `IItemStore` 中。与 `ITileRawData` 具有 `num`(图块数字)、`id`(字符串标识符)、`type`(逻辑类型)等字段一样,`IItemRawData` 存储道具的项目级静态定义。该接口已存在于 `store/types.ts` 中(当前为空占位),需要补齐字段。 + +`IItemStore` 作为 Layer 0 的基础设施被 `IDataCommon` 持有,与 `ITileStore` 平级。后续 `IItemManager`(Layer 1)通过 `IDataCommonExtended.state.itemStore` 访问原始数据,据此创建 `IItem` 运行时实例。 + +### 接口分析 + +- `IItemRawData.num`:预期频率**高频**。道具在地图上的图块数字,是地图→道具的核心关联键。典型使用场景:事件触发器读取地图上某图块对应的道具信息。 +- `IItemRawData.id`:预期频率**高频**。道具的字符串唯一标识符。典型使用场景:通过 id 跨地图引用道具。 +- `IItemRawData.category`:预期频率**中频**。道具分类,决定使用时的基础行为分支。典型使用场景:拾取道具时判断是否为 `Pick` 类以立即触发效果。 +- `IItemRawData.name`:预期频率**高频**。道具显示名称,UI 展示的核心字段。 +- `IItemRawData.text`:预期频率**低频**。道具描述文本,仅详情展示时出现。 + +### 预期体量 + +`IItemRawData` 为纯数据接口,约 10–12 行。 + +## IItemLegacyConverter + +### 设计意图 + +参考 `ITileLegacyConverter`,定义从旧样板道具对象到 `IItemRawData` 的转换器接口。与后续 `IItemLegacyBridge` 不同——`IItemLegacyConverter` 位于 store 层负责原始数据的导入,`IItemLegacyBridge`(Layer 1)负责运行时属性的转换。二者分层独立,分别处理静态数据与运行时对象。 + +### 接口分析 + +- `IItemLegacyConverter.fromLegacy`:预期频率**低频**。仅在样板加载阶段调用,执行旧格式→新格式的批量转换。 + +### 预期体量 + +一个方法,约 8–10 行。 + +## IItemStore + +### 设计意图 + +参考 `ITileStore` 的接口模式,作为道具原始数据的集中存储。道具本身是一类 tile 的具体属性,通过 tile 即可查找到对应的道具信息,因此不需要 `ITileStore` 那样的双向 id↔num 索引。但数据内仍存储 `id` 与 `num`,方便在需要时进行查询。 + +### 接口分析 + +- `IItemStore.getData`:预期频率**高频**。按图块数字获取完整原始数据,是查询道具定义的最常用入口。典型使用场景:地图图层解析到某图块数字后读取道具定义。 +- `IItemStore.getCategory`:预期频率**中频**。快速判断道具分类,避免完整获取数据。典型使用场景:拾取判定时只关心是否为 `Pick` 类型。 +- `IItemStore.addItem`:预期频率**低频**。添加一个道具原始定义,仅在样板加载时调用。 +- `IItemStore.attachLegacyConverter`:预期频率**低频**。挂载旧样板转换器,初始化阶段调用。 +- `IItemStore.fromLegacy`:预期频率**低频**。使用当前转换器转换并写入一个旧样板道具定义。 + +### 预期体量 + +接口约 20–25 行,实现类 `ItemStore` 约 50–70 行(内部仅维护 `dataMap: Map` 一张表,与 `TileStore` 相比少了双向索引表)。 + +# 涉及文件 + +## 需要引用的文件 + +- `@user/data-common/src/store/types.ts`:参考 `ITileRawData`、`ITileLegacyConverter`、`ITileStore` 的接口模式,用于设计 `IItemStore`。 +- `@user/data-common/src/store/tileStore.ts`:参考 `TileStore` 的实现模式,用于实现 `ItemStore`。 + +## 需要修改的文件 + +### `@user/data-common/src/store/types.ts` + +- [x] 新增 `ItemCategory` 枚举:包含 `Unknown`、`Constant`、`Consumable`、`Pick`、`Equipment` 五个成员。放在 `store/types.ts` 以避免与 `types.ts` 的循环引用。 +- [x] 补齐 `IItemRawData` 接口(原为空占位):新增 `num`、`id`、`category`、`name`、`text` 字段。 +- [x] 新增 `IItemLegacyConverter` 接口:定义旧样板道具→`IItemRawData` 的转换方法。 +- [x] 新增 `IItemStore` 接口:提供 `getData`、`getCategory`、`addItem`、`attachLegacyConverter`、`fromLegacy` 方法,不需要 id↔num 双向索引。 + +### `@user/data-common/src/store/itemStore.ts`(新建) + +- [x] 新增 `ItemStore` 类:实现 `IItemStore` 接口,参考 `TileStore` 的实现模式。 +- [x] 内部维护 `dataMap: Map` 一张表,无需 id↔num 双向映射。 +- [x] 实现 `fromLegacy`:无 converter 时抛出错误。 + +### `@user/data-common/src/store/index.ts` + +- [x] 新增 `export * from './itemStore'` 导出。 + +### `@user/data-common/src/types.ts` + +- [x] 修改 `IDataCommon` 接口:新增 `readonly itemStore: IItemStore` 成员。 diff --git a/docs/dev/item/item-system.md b/docs/dev/item/item-system.md new file mode 100644 index 0000000..8f50ab4 --- /dev/null +++ b/docs/dev/item/item-system.md @@ -0,0 +1,157 @@ +# 需求综述 + +开发道具系统,核心包括以下内容: + +1. **道具分类**:道具分为四种类型——`constant` 永久道具、`consumable` 一次性道具、`pick` 即捡即用道具、`equipment` 装备类道具。 +2. **旧样板兼容**:参考 `packages-user/data-state/src/enemy/legacy.ts` 的模式,定义旧样板兼容接口,不关心旧样板内部存储格式。 +3. **道具事件与道具脚本**:事件是面向新手的低代码自定义功能(该子系统尚未构建,仅提供接口占位);脚本则传入函数,用于高级用户自定义道具行为。 + +整体架构遵循现有模式:Layer 0(`data-common`)定义 `ItemCategory` 枚举与 `IItemStore` 存储层(见 [item-store.md](./item-store.md)),Layer 1(`data-base`)定义核心运行时接口与实现类,Layer 2(`data-system`)为执行引擎层,Layer 3(`data-state`)提供旧样板兼容桥接实现等顶层集成。 + +# 接口设计分析 + +## IItemAttr(Layer 1 - 运行时属性) + +### 设计意图 + +道具的基础属性接口,定义所有道具共有的字段。与 `IEnemyAttr` 类似,作为道具数据的核心载体。道具分类不同会导致其附加能力差异较大(如装备类有攻防加成、消耗类有恢复量等),因此 `IItemAttr` 仅包含所有道具共有的最小属性集合,分类特有的属性通过事件 / 脚本机制覆盖。 + +### 接口分析 + +- `IItemAttr.id`:预期频率**高频**。道具的唯一标识,在查询道具模板、存档读档等场景中频繁使用。典型使用场景:通过 id 获取指定道具模板。 +- `IItemAttr.name`:预期频率**高频**。道具显示名称,几乎所有 UI 展示场景都需要。典型使用场景:物品栏、商店、拾取提示中显示道具名称。 +- `IItemAttr.category`:预期频率**中频**。分类决定道具的基础行为模式(拾取后是否消耗、是否能装备等),在使用道具时作为关键判断依据。典型使用场景:判断道具是否可以装备到装备栏。 +- `IItemAttr.text`:预期频率**低频**。道具描述文本,仅在详情展示中出现。典型使用场景:鼠标悬浮或点击时的 Tooltip。 + +### 预期体量 + +`IItemAttr` 仅为一个简单的属性接口,预计约 10–15 行。 + +## IReadonlyItem / IItem + +### 设计意图 + +遵循怪物的只读 / 可变双接口模式:`IReadonlyItem` 提供读访问,`IItem` 继承前者并扩展写操作与 `ISaveableContent`。道具的核心能力(事件与脚本)挂载于 `IItem` 上。 + +### 接口分析 + +- `IReadonlyItem.id`:预期频率**高频**。同 `IItemAttr.id`,在只读场景下主要使用的访问方式。 +- `IReadonlyItem.code`:预期频率**中频**。道具在地图上的图块数字,用于从地图图块关联到道具模板。典型使用场景:处理事件触发器从地图上读取道具信息。 +- `IReadonlyItem.category`:预期频率**中频**。判断道具类别以决定后续行为。 +- `IReadonlyItem.getAttribute`:预期频率**中频**。获取道具属性的指定字段,与怪物接口保持一致。典型使用场景:战斗计算中读取装备道具提供的属性加成。 +- `IItem.addEvent`:预期频率**低频**。添加事件配置(低代码),通常由样板编辑器生成。由于事件系统尚未构建,此处仅提供接口占位。 +- `IItem.addScript`:预期频率**低频**。添加脚本函数,通常由高级用户在代码中直接编写。 +- `IItem.clone`:预期频率**中频**。深拷贝道具对象,用于创建怪物掉落物的独立实例。典型使用场景:战斗结算后为怪物掉落道具生成独立副本。 + +### 预期体量 + +两个接口合计约 30–40 行,其中 `IItem` 主要是在 `IReadonlyItem` 基础上增加写操作与 `ISaveableContent` 实现。 + +## IItemEvent 与 IItemScript + +### 设计意图 + +道具自定义行为的两种途径: + +- **IItemEvent**:面向新手的低代码配置。用户通过编辑器配置预设事件类型及参数,运行时由事件引擎解析执行。当前事件引擎尚未构建,本接口仅作为占位,后续事件系统实现后再对接。 +- **IItemScript**:面向高级用户的函数式定制。用户直接传入函数,由道具系统在适当时机调用。脚本持有道具自身的引用,可在函数内通过 `item` 访问道具信息。 + +### 接口分析 + +- `IItemEvent.type`:预期频率**高频(编辑器层面)**。事件类型标识,决定事件引擎如何解析该事件。典型使用场景:样板编辑器中下拉选择"回复 HP"事件类型。 +- `IItemEvent.params`:预期频率**高频**。事件参数,与 type 配合决定具体效果。 +- `IItemScript.execute`:预期频率**中频**。执行脚本函数时的入口。典型使用场景:道具使用触发自定义逻辑(如播放动画后增减属性)。 + +### 预期体量 + +两个接口合计约 15–20 行,当前阶段仅需定义最小化的接口形状。 + +## IItemSaveState / IItemManagerSaveState + +### 设计意图 + +存档状态接口,与 `IEnemySaveState` / `IEnemyManagerSaveState` 对应的设计。`IItemSaveState` 保存单个道具的完整状态快照,`IItemManagerSaveState` 保存管理器中所有变更的模板。事件与脚本是否需要存档取决于其可变性 — 如果事件配置与脚本函数在运行时不可变,则无需存档。 + +### 预期体量 + +两个接口合计约 15–20 行。 + +## IItemLegacyBridge + +### 设计意图 + +参考 `IEnemyLegacyBridge` 的设计,定义从旧样板道具对象到新道具属性的转换接口。不关心旧样板数据结构,仅定义方法签名。 + +### 接口分析 + +- `IItemLegacyBridge.fromLegacyItem`:预期频率**低频**。仅在样板加载时调用一次,用于将所有旧样板道具转换为新格式。仅此一方法,不存在中频场景。 + +### 预期体量 + +仅一个方法,约 8–12 行。 + +## IItemManager + +### 设计意图 + +遵循 `IEnemyManager` 的 Manager 模式,管理道具模板(prefab),负责创建、注册、旧样板导入及存档读档。事件注册与脚本注册暂不纳入 Manager(当前事件系统未构建,脚本为函数引用无需注册),但预留接口位置。 + +### 接口分析 + +- `IItemManager.createItem`:预期频率**中频**。根据图块数字创建道具实例,是战斗掉落、商店购买等场景的入口。典型使用场景:战斗结算时根据怪物掉落表 code 创建道具。 +- `IItemManager.addPrefab`:预期频率**低频**。添加道具模板,样板加载阶段调用。 +- `IItemManager.addPrefabFromLegacy`:预期频率**低频**。从旧样板导入道具模板,样板加载阶段调用。 +- `IItemManager.fromLegacyItem`:预期频率**低频**。从旧样板道具对象转换。 +- `IItemManager.getPrefab`:预期频率**中频**。获取道具模板的只读视图。典型使用场景:UI 查询道具信息用于展示。 + +### 预期体量 + +接口约 60–80 行,实现类约 200–300 行(包含 prefab 管理、dirty 追踪、存档读档逻辑,与 `EnemyManager` 体量相当)。 + +# 问题 + +1. `IItemAttr` 当前仅设计了 `id`、`name`、`category`、`text` 四个字段,是否需要额外添加分类相关的通用字段(如道具对应图块数字 `code`)?`code` 目前放在了 `IReadonlyItem` 层而非 `IItemAttr` 层,这样是否合理? +2. 对于装备类道具,是否需要一个独立的 `IEquipmentAttr` 接口来承载攻防加成等属性?还是这些应该完全通过事件 / 脚本机制实现? +3. `IItemEvent` 的事件类型标识应使用字符串还是枚举?按照开发规范应使用枚举,但事件系统尚未构建,此时定义枚举存在不确定性。建议先定义为 `string` 留白,待事件系统构建后再替换为枚举? +4. 道具的数据是否应该进入存档?如果道具在游戏中不可变(即模板数据不变化),则无需存档。但考虑到可能存在"道具数量"这类可变状态,是否需要单独的状态管理?还是数量管理不属于道具系统本身的职责? +5. 道具与背包系统之间的关系如何处理?背包系统是否独立于道具系统?还是道具系统需要预留背包相关接口? + +# 涉及文件 + +## 需要引用的文件 + +- `@user/data-common/src/save/types.ts`:引用 `ISaveableContent`、`SaveCompression`。 +- `@user/data-base/src/enemy/types.ts`:参考 `IEnemyManager` 和 `IEnemyLegacyBridge` 的接口模式。 + +## 需要修改的文件 + +### `@user/data-common/src/types.ts` + +- [ ] 新增 `IItemAttr` 接口:定义道具运行时属性的基础字段。 + +### `@user/data-base/src/item/types.ts`(新建) + +- [ ] 新增 `IItemSaveState` 接口:单个道具的存档格式。 +- [ ] 新增 `IItemManagerSaveState` 接口:道具管理器的存档格式。 +- [ ] 新增 `IReadonlyItem` 接口:只读道具接口。 +- [ ] 新增 `IItem` 接口:可变道具接口,继承 `IReadonlyItem` 和 `ISaveableContent>`。 +- [ ] 新增 `IItemEvent` 接口:事件配置占位接口。 +- [ ] 新增 `IItemScriptFn` 类型别名:道具脚本函数签名。 +- [ ] 新增 `IItemLegacyBridge` 接口:旧样板兼容桥接接口。 +- [ ] 新增 `IItemManager` 接口:道具管理器接口,继承 `ISaveableContent>`。 + +### `@user/data-base/src/item/enum.ts`(新建,如需要) + +- [ ] 若 `IItemEvent.type` 需要枚举,在此定义事件类型枚举。 + +### `@user/data-base/src/item/index.ts`(新建) + +- [ ] 新增 barrel 导出,汇总导出 `types.ts` 中的所有内容。 + +### `@user/data-base/src/index.ts` + +- [ ] 新增 `export * from './item'` 导出。 + +### `@user/data-base/src/types.ts` + +- [ ] 修改 `IStateBase` 接口:新增 `readonly itemManager: IItemManager` 成员。 diff --git a/packages-user/data-common/src/store/index.ts b/packages-user/data-common/src/store/index.ts index b86cf0f..9d209b3 100644 --- a/packages-user/data-common/src/store/index.ts +++ b/packages-user/data-common/src/store/index.ts @@ -1,2 +1,3 @@ +export * from './itemStore'; export * from './tileStore'; export * from './types'; diff --git a/packages-user/data-common/src/store/itemStore.ts b/packages-user/data-common/src/store/itemStore.ts new file mode 100644 index 0000000..0f00696 --- /dev/null +++ b/packages-user/data-common/src/store/itemStore.ts @@ -0,0 +1,42 @@ +import { logger } from '@motajs/common'; +import { + IItemLegacyConverter, + IItemRawData, + IItemStore, + ItemCategory +} from './types'; + +export class ItemStore implements IItemStore { + /** 以道具图块数字为键的原始道具定义表 */ + private readonly dataMap: Map = new Map(); + + /** 当前挂载的旧样板道具转换器 */ + private converter: IItemLegacyConverter | null = null; + + getData(num: number): IItemRawData | null { + return this.dataMap.get(num) ?? null; + } + + getCategory(num: number): ItemCategory { + return this.dataMap.get(num)?.category ?? ItemCategory.Unknown; + } + + addItem(data: IItemRawData): void { + this.dataMap.set(data.num, data); + } + + attachLegacyConverter(converter: IItemLegacyConverter): void { + this.converter = converter; + } + + fromLegacy(num: number, legacy: TLegacy): IItemRawData { + const converter = this.converter; + if (!converter) { + logger.error(57); + throw new Error('Expected a item legacy converter.'); + } + const data = converter.fromLegacy(num, legacy); + this.addItem(data); + return data; + } +} diff --git a/packages-user/data-common/src/store/tileStore.ts b/packages-user/data-common/src/store/tileStore.ts index af88d98..9b77d7a 100644 --- a/packages-user/data-common/src/store/tileStore.ts +++ b/packages-user/data-common/src/store/tileStore.ts @@ -70,7 +70,7 @@ export class TileStore implements ITileStore { const converter = this.legacyConverter; if (!converter) { logger.error(56); - throw new Error('Expected a tile legacy converter'); + throw new Error('Expected a tile legacy converter.'); } const data = converter.fromLegacy(num, legacy); this.addTile(data); diff --git a/packages-user/data-common/src/store/types.ts b/packages-user/data-common/src/store/types.ts index 945ceee..c3cd52b 100644 --- a/packages-user/data-common/src/store/types.ts +++ b/packages-user/data-common/src/store/types.ts @@ -68,7 +68,7 @@ export interface ITileLegacyConverter { fromLegacy(num: number, legacy: TLegacy): ITileRawData; } -export interface ITileStore { +export interface ITileStore { /** * 获取指定图块数字对应的完整原始定义 * @param num 图块数字 @@ -120,3 +120,88 @@ export interface ITileStore { } //#endregion + +//#region item + +export const enum ItemCategory { + /** 未知或尚未归类的道具 */ + Unknown, + /** 永久道具,使用后不会消耗 */ + Constant, + /** 一次性道具,使用后消耗 */ + Consumable, + /** 即捡即用道具,捡到立刻生效 */ + Pick, + /** 装备类道具,可装备 / 卸下 */ + Equipment +} + +export interface IItemRawData { + /** 道具在地图上的图块数字 */ + readonly num: number; + /** 道具的字符串标识符 */ + readonly id: string; + /** 道具分类 */ + readonly category: ItemCategory; + /** 道具显示名称 */ + readonly name: string; + /** 道具描述文本 */ + readonly text: string; + + /** 即捡即用效果(Pick 类型) */ + readonly pickEffect: string; + /** 即捡即用事件(Pick 类型) */ + readonly pickEvent: unknown; + /** 道具使用效果(Constant 和 Consumable 类型) */ + readonly useEffect: string; + /** 道具使用事件(Constant 和 Consumable 类型) */ + readonly useEvent: unknown; + /** 是否能够使用道具(Constant 和 Consumable 类型) */ + readonly canUse: string; + /** 装备道具属性(Equipment 类型) */ + readonly equip: unknown; +} + +export interface IItemLegacyConverter { + /** + * 将旧样板道具定义转换为新的道具原始数据 + * @param num 道具图块数字 + * @param legacy 旧样板道具定义 + */ + fromLegacy(num: number, legacy: TLegacy): IItemRawData; +} + +export interface IItemStore { + /** + * 获取指定图块数字对应的道具原始数据 + * @param num 道具图块数字 + */ + getData(num: number): IItemRawData | null; + + /** + * 获取指定图块数字对应的道具分类 + * @param num 道具图块数字 + */ + getCategory(num: number): ItemCategory; + + /** + * 添加一个道具原始数据定义 + * @param data 道具原始数据 + */ + addItem(data: IItemRawData): void; + + /** + * 挂载一个旧样板转换器 + * @param converter 旧样板转换器 + */ + attachLegacyConverter(converter: IItemLegacyConverter): void; + + /** + * 使用当前转换器转换并写入一个旧样板道具定义 + * @param num 道具图块数字 + * @param legacy 旧样板道具定义 + */ + fromLegacy(num: number, legacy: TLegacy): IItemRawData; +} + +//#endregion diff --git a/packages-user/data-common/src/types.ts b/packages-user/data-common/src/types.ts index b35d8c6..179f9df 100644 --- a/packages-user/data-common/src/types.ts +++ b/packages-user/data-common/src/types.ts @@ -1,6 +1,6 @@ import { ITileLocator } from '@motajs/common'; import { IFaceManager, IRoleFaceBinder } from './common'; -import { ITileStore } from './store'; +import { IItemStore, ITileStore } from './store'; import { ISaveSystem } from './save'; export interface IEnemyAttr { @@ -46,6 +46,8 @@ export interface IHeroAttr { export interface IDataCommon { /** 图块定义存储 */ readonly tileStore: ITileStore>; + /** 道具定义存储 */ + readonly itemStore: IItemStore; /** 朝向绑定 */ readonly roleFace: IRoleFaceBinder; /** 朝向管理 */ diff --git a/packages/common/src/logger.json b/packages/common/src/logger.json index 0177e22..ccb30aa 100644 --- a/packages/common/src/logger.json +++ b/packages/common/src/logger.json @@ -55,7 +55,8 @@ "53": "Expected serializable value set as enemy's default attribute.", "54": "Legacy '$1' API has been removed, consider using new APIs: '$2'.", "55": "Cannot load MapStore state: reference data (compareWith) has not been set.", - "56": "Cannot convert legacy tile data since no tile legacy converter is attached to TileStore." + "56": "Cannot convert legacy tile data since no tile legacy converter is attached to TileStore.", + "57": "Cannot convert legacy item data since no item legacy converter is attached to ItemStore." }, "warn": { "1": "Resource with type of 'none' is loaded.", diff --git a/prompt.md b/prompt.md index 584ca3f..7182225 100644 --- a/prompt.md +++ b/prompt.md @@ -37,7 +37,7 @@ 3. **发现接口问题时提问**:若认为类型标注中的接口设计有问题,或在实现中发现缺少某些接口,应向我提问是否添加,经我同意后方可添加。 4. **接口设计兼顾合理性与便捷性**:设计接口时不仅要考虑合理性,还要考虑使用便捷性。罕见场景应当被支持,但不应与常见场景共用同一接口——这只会增加常见场景的使用难度。 5. **避免多余的非空判断与类型守卫**:类型已满足约束时不应再额外判断或过滤。例如 `Promise.all` 接受 `(Promise | void)[]`,无需写成 `Promise.all(arr.filter(v => !!v))`。 -6. **关于整个仓库的类型检查**:单个需求**不需要**跑全仓库的类型检查,仅在重构时需要。 +6. **关于整个仓库的类型检查**:单个需求**不需要**跑全仓库的类型检查,仅在重构时需要。如果跑类型检查,由于系统构建的不完全,可能包含一部分其他系统的错误信息,自行辨别。 # 开发流程