基于《AI 开发规范:魔女炼金工坊 V2.0》实现的跨平台小游戏(Water Sort 倒水玩法,魔法炼金主题)。支持微信小游戏、微信小程序、抖音小程序等多平台。
核心技术栈:PixiJS 8 + TypeScript + Vite + GSAP
平台适配层:PlatformAdapter(屏蔽平台差异,统一 Canvas 渲染 + 本地存储)
开发原则:AI First + 配置驱动 + 数据优先
| 维度 | 说明 |
|---|---|
| 游戏类型 | 休闲益智 / Water Sort 倒水玩法 |
| 主题 | 女巫炼金实验室 |
| 目标用户 | 女性向,18-40 岁 |
| 单局时长 | 1-5 分钟 |
| 变现方式 | 广告变现 + 皮肤销售 |
| 目标平台 | ✅ 微信小游戏 / ✅ 微信小程序 / ✅ 抖音小程序 / ✅ H5 |
如果已有模块能够扩展,则禁止创建新模块。
// ✅ 正确:修改现有模块
LevelManager 新增方法 generateDynamicLevel()
// ❌ 错误:创建新模块
新建 AdvancedLevelManager禁止代码决定行为,必须 JSON 决定行为。
// ❌ 错误
if (level === 20) {
giveReward(1000);
}
// ✅ 正确
const levelConfig = await loadConfig(`levels/${level}.json`);
giveReward(levelConfig.reward);配置覆盖范围:
- 关卡难度 / 配置
- 女巫升级体系
- 广告策略
- 音效 / 动画参数
- 颜色 / UI 尺寸
逻辑不在代码中,在数据中。
// ❌ 错误:硬编码逻辑
if (steps <= 5) stars = 3;
else if (steps <= 8) stars = 2;
else if (steps <= 12) stars = 1;
// ✅ 正确:数据驱动
const criteria = levelConfig.starCriteria;
const stars = calculateStars(steps, criteria);以后美术替换:不用改代码。
// ✅ 正确做法
// 1. 所有动画参数存放在 config
// 2. 动画逻辑在 AnimationManager
// 3. Bottle 只调用 AnimationManager.play(name)
// 美术替换时:只改 config 和资源,不改代码统一通过 AssetsManager 加载。
// ❌ 错误
const image = await PIXI.Assets.load('./assets/ui/button.png');
// ✅ 正确
const image = assetsManager.get('button');
// AssetsManager 内部管理映射:'button' → 真实路径禁止浏览器专属 API,必须通过 PlatformAdapter。
// ❌ 错误
const isMobile = /iPhone|iPad|Android/.test(navigator.userAgent);
// ✅ 正确
const isMobile = await platformAdapter.isMobile();Manager 不直接引用 Manager,统一通过依赖注入或事件通信。
// ❌ 错误:循环引用
class LevelManager {
saveManager.saveLevelProgress(); // 直接调用
}
// ✅ 正确:事件驱动
class LevelManager {
events.emit('levelComplete', data);
}
class GameManager {
levelManager.on('levelComplete', (data) => {
saveManager.save(data);
audioManager.playSound('levelup');
});
}- 用「导入项目」时,目录必须选
dist/(编译产物),不要选witch-alchemy/源码根目录 - 源码根目录只有
game.json/project.config.json,没有game.js入口,开发者工具会回退成「小程序」模式去找app.json,报「未找到 app.json」 dist/是完整自包含的小游戏工程:game.js+game.json+project.config.json(compileType: "game")- AppID 必须是「小游戏」类型(或在导入框点「使用测试号」)。若用了「小程序」AppID,开发者工具会按 AppID 注册类型强制小程序模式,直接忽略
compileType: "game" appid千万别填「小程序」AppID。小游戏项目绑了小程序 AppID 时,微信会做gameTransfer校验,类型不匹配直接被后台拒绝。解决:清空appid后重导、选测试号- 项目类型在「导入/创建」那一刻锁定,之后不会重读
compileType切换。若已被识别成小程序,必须:① 在项目列表右键「移除项目」;② 重启开发者工具;③ 重新「导入项目」选dist/+ 测试号。仅改project.config.json无法切换 - 若之前导入过导致缓存了错误项目类型:删除
dist/project.private.config.json后重新导入即可
| 组件 | 选型 | 说明 |
|---|---|---|
| 语言 | TypeScript(严格模式) | 禁止 any,全量类型检查 |
| 渲染引擎 | PixiJS 8 | 原生 Canvas,性能优秀,跨平台 |
| 动画 | GSAP 3 | 补间/时间轴,配合 Pixi Ticker |
| 构建工具 | Vite | 开发模式原生 ESM,生产 CJS 包(dist/game.js) |
| 平台适配 | PlatformAdapter | 统一 Canvas/Storage API,支持微信/抖音/浏览器 |
| 存储 | LocalStorage(可扩展为 CloudAPI) | 本地优先,后续无缝迁移到云端 |
| 音频 | Web Audio API(平台垫片) | 延迟初始化,避免微信/抖音的音频黑名单 |
跨平台兼容性: ✅ 微信小游戏 / ✅ 微信小程序(WebView) / ✅ 抖音小程序 / ✅ 浏览器 / ✅ iOS Safari
| 约束 | 检查项 | 状态 |
|---|---|---|
| 全量 TypeScript | tsconfig.json strict: true |
✅ |
禁止 any |
ESLint rule: @typescript-eslint/no-explicit-any |
✅ |
| 不使用 React/Vue 渲染游戏 | 所有 UI 使用 Pixi Container |
✅ |
| 所有配置 JSON 化 | src/core/config/ |
✅ |
| 不直接耦合平台 API | 统一封装于 PlatformAdapter |
✅ |
| 数据层与 UI 层分离 | Manager 模式 + 事件驱动 | ✅ |
| 所有动画使用 GSAP | 禁止 setInterval / setTimeout / requestAnimationFrame |
✅ |
| 资源统一管理 | AssetsManager | ✅ |
开始游戏
↓
进入关卡
↓
完成关卡 → 获得金币 + 魔法经验
↓
升级女巫 → 解锁新皮肤 / 实验室
↓
解锁下一关
↓
无限循环
| 阶段 | 关卡范围 | 说明 | 生成方式 |
|---|---|---|---|
| 教学 | 1-20 | 手动设计,难度递进 | 手动 |
| 普通 | 21-60 | 基础玩法,稳定难度 | 手动 |
| 特殊机制 | 61-120 | 引入冰冻/锁链等特殊瓶子 | 手动 |
| 组合机制 | 121-200 | 多种机制混合 | 手动 |
| 无限挑战 | 200+ | 自动生成,难度无限递进 | 动态 |
取消时间限制,仅看步数:
interface LevelStarCriteria {
levelId: number;
optimalSteps: number; // 3 星:完美(最优步数)
acceptableSteps: number; // 2 星:优秀(可接受范围)
maxSteps: number; // 1 星:完成(通关最多步数)
}
function calculateStars(steps: number, criteria: LevelStarCriteria): number {
if (steps <= criteria.optimalSteps) return 3; // ⭐⭐⭐ 完美
if (steps <= criteria.acceptableSteps) return 2; // ⭐⭐ 优秀
if (steps <= criteria.maxSteps) return 1; // ⭐ 完成
return 0; // 失败
}witch-alchemy/
├── src/
│ ├── core/
│ │ ├── managers/
│ │ │ ├── GameManager.ts
│ │ │ ├── SceneManager.ts
│ │ │ ├── LevelManager.ts
│ │ │ ├── SaveManager.ts
│ │ │ ├── AudioManager.ts
│ │ │ ├── AdManager.ts
│ │ │ ├── AssetsManager.ts
│ │ │ └── PlatformAdapter.ts
│ │ ├── scenes/
│ │ │ ├── LoadingScene.ts
│ │ │ ├── HomeScene.ts
│ │ │ ├── GameScene.ts
│ │ │ └── ResultScene.ts
│ │ ├── objects/
│ │ │ ├── Bottle.ts
│ │ │ ├── Liquid.ts
│ │ │ ├── Witch.ts
│ │ │ └── Cat.ts
│ │ ├── ui/
│ │ │ ├── Button.ts
│ │ │ ├── Dialog.ts
│ │ │ └── HUD.ts
│ │ ├── config/
│ │ │ ├── levels.json
│ │ │ ├── assets.json
│ │ │ ├── ads.json
│ │ │ └── constants.ts
│ │ ├── api/
│ │ │ ├── storage.ts
│ │ │ ├── cloud.ts
│ │ │ └── events.ts
│ │ ├── app/
│ │ │ ├── GameApp.ts
│ │ │ └── context.ts
│ │ └── types.ts
│ ├── assets/
│ │ ├── sounds/
│ │ ├── ui/
│ │ └── sprites/
│ ├── main.ts
│ └── index.html
├── dist/
├── package.json
├── tsconfig.json
├── vite.config.ts
├── eslint.config.js
└── README.md
// types.ts
interface GameContext {
// ========== 配置 ==========
levelConfig: LevelConfig[];
assetsConfig: AssetsConfig;
adsConfig: AdsConfig;
saveStrategy: SaveStrategy;
// ========== Managers ==========
gameManager: GameManager;
sceneManager: SceneManager;
levelManager: LevelManager;
saveManager: SaveManager;
audioManager: AudioManager;
adManager: AdManager;
assetsManager: AssetsManager;
platformAdapter: PlatformAdapter;
events: EventEmitter;
// ========== 运行时数据 ==========
userData: UserData;
currentLevel: number;
currentScene: string;
isLoading: boolean;
}
// 全局访问
declare global {
var gameContext: GameContext;
}| Manager | 职责 | 禁止 |
|---|---|---|
| GameManager | 全局游戏状态机、Manager 调度 | 直接改数据 |
| SceneManager | 场景加载、切换、销毁 | 业务逻辑 |
| LevelManager | 关卡加载、难度控制、胜负判定 | 保存数据 |
| SaveManager | 本地/云端存储、数据同步 | 游戏逻辑 |
| AudioManager | 音效播放、音量管理、BGM 控制 | 音效合成 |
| AdManager | 广告加载、展示、回调处理 | 强制广告 |
| AssetsManager | 资源映射、加载、缓存管理 | 路径依赖 |
| PlatformAdapter | 平台 API 适配、容错处理 | 业务逻辑 |
GameManager(全局调度中枢)
│
├─→ SceneManager
│ ├─→ HomeScene
│ │ └─ 依赖:LevelManager, SaveManager, AssetsManager
│ ├─→ GameScene
│ │ └─ 依赖:LevelManager, AudioManager, AdManager
│ ├─→ ResultScene
│ │ └─ 依赖:SaveManager, AudioManager, AdManager
│ └─→ LoadingScene
│ └─ 依赖:AssetsManager
│
├─→ LevelManager
│ └─ 内部依赖:AssetsManager
│
├─→ SaveManager
│ └─ 内部依赖:PlatformAdapter, StorageAPI
│
├─→ AudioManager
│ └─ 内部依赖:PlatformAdapter
│
├─→ AdManager
│ └─ 内部依赖:PlatformAdapter
│
├─→ AssetsManager
│ └─ 内部依赖:PlatformAdapter, PIXI.Assets
│
└─→ PlatformAdapter(所有 Manager 最终依赖)
├─ Canvas / WebGL 接口
├─ Storage 接口
├─ 广告 API
└─ 平台检测
需要的 Manager:
class HomeScene {
constructor(
private levelManager: LevelManager,
private saveManager: SaveManager,
private assetsManager: AssetsManager,
private audioManager: AudioManager,
private events: EventEmitter
) {}
async create() {
// 加载女巫数据
const userData = await saveManager.loadUserData();
// 加载关卡进度
const levelProgress = await saveManager.loadLevelProgress(userData.currentLevel);
// 获取资源
const backgroundImage = assetsManager.get('home_bg');
}
}需要的 Manager:
class GameScene {
constructor(
private levelManager: LevelManager,
private saveManager: SaveManager,
private audioManager: AudioManager,
private adManager: AdManager,
private events: EventEmitter
) {}
async create() {
// 加载关卡
const levelConfig = await levelManager.loadLevel(this.levelId);
// 监听关卡完成事件
this.events.on('levelComplete', (data) => {
this.handleLevelComplete(data);
});
}
private async handleLevelComplete(data: GameplayResult) {
// 保存进度
await this.saveManager.saveLevelProgress(data);
// 播放音效
this.audioManager.play('levelup');
// 切换到结果页
this.events.emit('sceneChange', 'result');
}
}// api/events.ts
export enum GameEvents {
// ========== 场景事件 ==========
SCENE_CHANGE = 'scene:change', // 切换场景
SCENE_CREATED = 'scene:created', // 场景创建
SCENE_DESTROYED = 'scene:destroyed', // 场景销毁
// ========== 关卡事件 ==========
LEVEL_START = 'level:start', // 关卡开始
LEVEL_COMPLETE = 'level:complete', // 关卡完成
LEVEL_FAILED = 'level:failed', // 关卡失败
LEVEL_RETRY = 'level:retry', // 关卡重试
LEVEL_SKIP = 'level:skip', // 关卡跳过(仅测试)
// ========== 瓶子事件 ==========
BOTTLE_SELECTED = 'bottle:selected', // 瓶子被选中
BOTTLE_DESELECTED = 'bottle:deselected', // 瓶子被取消选中
BOTTLE_POURED = 'bottle:poured', // 倒水完成
BOTTLE_INVALID = 'bottle:invalid', // 无效操作
// ========== 游戏状态事件 ==========
GAME_OVER = 'game:over', // 游戏结束
GAME_PAUSE = 'game:pause', // 游戏暂停
GAME_RESUME = 'game:resume', // 游戏继续
// ========== 存储事件 ==========
SAVE_START = 'save:start', // 保存开始
SAVE_COMPLETE = 'save:complete', // 保存完成
SAVE_FAILED = 'save:failed', // 保存失败
// ========== UI 事件 ==========
UI_SHOW_DIALOG = 'ui:show_dialog', // 显示对话框
UI_HIDE_DIALOG = 'ui:hide_dialog', // 关闭对话框
UI_UPDATE_HUD = 'ui:update_hud', // 更新 HUD
}当玩家通关时,各 Manager 的执行顺序很重要,必须避免数据竞争:
// GameManager 中的处理逻辑
async function handleLevelComplete(result: GameplayResult): Promise<void> {
try {
// 1️⃣ 最高优先级:保存数据(防止数据丢失)
console.log('1. 保存进度');
await saveManager.saveLevelProgress(result);
events.emit(GameEvents.SAVE_COMPLETE);
// 2️⃣ 高优先级:更新游戏状态
console.log('2. 判定胜负和奖励');
const nextLevelUnlocked = await levelManager.onLevelComplete(result.levelId);
// 3️⃣ 中优先级:播放音效(非阻塞)
console.log('3. 播放音效');
audioManager.play('levelup');
// 4️⃣ 中优先级:展示广告(可能耗时)
console.log('4. 展示广告');
await adManager.showRewardedAd({
onSuccess: () => console.log('广告观看成功'),
onFailed: () => console.log('广告展示失败,直接发放奖励'),
});
// 5️⃣ 低优先级:切换场景
console.log('5. 切换场景');
await sceneManager.goto('result', { result, nextLevelUnlocked });
} catch (error) {
console.error('关卡完成处理失败:', error);
// 降级处理:至少保证数据已保存
await sceneManager.goto('result', { result, error });
}
}执行顺序规则:
- ⭐⭐⭐ 必须先保存数据
- ⭐⭐ 然后更新游戏状态
- ⭐ 最后处理 UI/声音/场景
{
"levels": [
{
"levelId": 1,
"difficulty": "easy",
"bottles": [
{
"bottleId": "bottle_0",
"type": "normal",
"liquids": [0, 1, 2]
},
{
"bottleId": "bottle_1",
"type": "normal",
"liquids": [1, 2]
},
{
"bottleId": "bottle_2",
"type": "normal",
"liquids": []
}
],
"starCriteria": {
"optimalSteps": 4,
"acceptableSteps": 6,
"maxSteps": 8
},
"unlockedBy": {
"type": "default"
},
"reward": {
"coins": 10,
"exp": 50
},
"tips": "Try to match colors"
},
{
"levelId": 2,
"difficulty": "easy",
"bottles": [
{
"bottleId": "bottle_0",
"type": "normal",
"liquids": [0, 1, 2, 3]
},
{
"bottleId": "bottle_1",
"type": "normal",
"liquids": [1, 2]
},
{
"bottleId": "bottle_2",
"type": "normal",
"liquids": []
}
],
"starCriteria": {
"optimalSteps": 5,
"acceptableSteps": 7,
"maxSteps": 10
},
"unlockedBy": {
"type": "previousLevel",
"levelId": 1,
"requiredStars": 1
},
"reward": {
"coins": 10,
"exp": 50
}
}
]
}// objects/BottleRule.ts
interface IBottleRule {
/**
* 检查是否可以从该瓶子倒出
* @param bottle 源瓶子
* @returns true 可以倒出,false 不可以
*/
canPourFrom(bottle: Bottle): boolean;
/**
* 检查是否可以倒入目标瓶子
* @param fromBottle 源瓶子
* @param toBottle 目标瓶子
* @returns true 可以倒入,false 不可以
*/
canPourTo(fromBottle: Bottle, toBottle: Bottle): boolean;
/**
* 执行倒水效果(如特殊瓶子的特殊处理)
* @param fromBottle 源瓶子
* @param toBottle 目标瓶子
*/
applyEffect(fromBottle: Bottle, toBottle: Bottle): void;
/**
* 获取瓶子的显示样式
* @returns CSS 类名或样式对象
*/
getStyle(): string | object;
}
// 不同瓶子类型的实现
class NormalBottleRule implements IBottleRule {
canPourFrom(bottle: Bottle): boolean {
return bottle.liquids.length > 0;
}
canPourTo(fromBottle: Bottle, toBottle: Bottle): boolean {
return toBottle.liquids.length < 4 &&
(toBottle.liquids.length === 0 ||
toBottle.liquids[toBottle.liquids.length - 1] === fromBottle.liquids[fromBottle.liquids.length - 1]);
}
applyEffect(fromBottle: Bottle, toBottle: Bottle): void {
// 普通瓶没有特殊效果
}
getStyle(): string {
return 'bottle-normal';
}
}
class FrozenBottleRule implements IBottleRule {
canPourFrom(bottle: Bottle): boolean {
return false; // 冰冻瓶不能倒出
}
canPourTo(fromBottle: Bottle, toBottle: Bottle): boolean {
return true; // 但可以被倒入
}
applyEffect(fromBottle: Bottle, toBottle: Bottle): void {
// 倒入后冰冻消融
toBottle.setType('frozen');
}
getStyle(): string {
return 'bottle-frozen';
}
}
class ChainBottleRule implements IBottleRule {
chainCount: number = 2;
canPourFrom(bottle: Bottle): boolean {
return this.chainCount === 0; // 链子都解开才能倒出
}
canPourTo(fromBottle: Bottle, toBottle: Bottle): boolean {
return toBottle.liquids.length < 4;
}
applyEffect(fromBottle: Bottle, toBottle: Bottle): void {
// 每次倒水链子减 1
this.chainCount--;
}
getStyle(): string {
return `bottle-chain chain-${this.chainCount}`;
}
}
// 在 GameScene 中使用
class GameScene {
private bottleRules: Map<string, IBottleRule> = new Map();
checkBottlePour(fromId: string, toId: string): boolean {
const fromRule = this.bottleRules.get(fromId);
const toBottle = this.bottles.get(toId);
return fromRule?.canPourTo(this.bottles.get(fromId)!, toBottle!) ?? false;
}
executePour(fromId: string, toId: string): void {
const fromBottle = this.bottles.get(fromId)!;
const toBottle = this.bottles.get(toId)!;
const rule = this.bottleRules.get(fromId)!;
// 执行倒水
rule.applyEffect(fromBottle, toBottle);
}
}// types.ts
interface SaveStrategy {
// ========== 何时保存 ==========
triggers: {
onLevelComplete: boolean; // 关卡完成时
onLevelFail: boolean; // 失败时
onLevelRetry: boolean; // 重试时
autoSaveInterval: number; // 自动保存间隔(毫秒),0 = 不自动保存
};
// ========== 保存位置 ==========
local: {
enabled: boolean; // 是否启用本地保存
storageKey: string; // LocalStorage 键名
};
cloud: {
enabled: boolean; // 是否启用云端同步
syncInterval: number; // 云端同步间隔(毫秒)
provider: 'wechat' | 'douyin' | 'custom'; // 云端提供商
};
// ========== 数据版本 ==========
version: number; // 存档版本,用于迁移
lastSyncTime: number; // 最后同步时间戳
}
// 默认配置
const DEFAULT_SAVE_STRATEGY: SaveStrategy = {
triggers: {
onLevelComplete: true,
onLevelFail: false,
onLevelRetry: false,
autoSaveInterval: 30000, // 30 秒自动保存一次
},
local: {
enabled: true,
storageKey: 'witch_alchemy_save',
},
cloud: {
enabled: false, // V1.0 不启用
syncInterval: 60000, // 60 秒同步一次
provider: 'wechat',
},
version: 1,
lastSyncTime: 0,
};// types.ts
type UnlockType = 'default' | 'previousLevel' | 'purchase' | 'event';
interface LevelUnlockRule {
levelId: number;
type: UnlockType;
// 当 type === 'previousLevel' 时
previousLevelId?: number; // 需要通过的前置关卡
requiredStars?: number; // 需要的最少星数(默认 1)
// 当 type === 'purchase' 时
cost?: number; // 需要花费的金币
// 当 type === 'event' 时
eventName?: string; // 事件名称
// 当 type === 'default' 时:无需额外条件
}
// 使用示例
const LEVEL_UNLOCK_RULES: LevelUnlockRule[] = [
{
levelId: 1,
type: 'default', // 第 1 关默认解锁
},
{
levelId: 2,
type: 'previousLevel',
previousLevelId: 1,
requiredStars: 1, // 通过第 1 关即可
},
{
levelId: 20,
type: 'previousLevel',
previousLevelId: 19,
requiredStars: 3, // 需要 3 星才能解锁
},
{
levelId: 100,
type: 'purchase',
cost: 500, // 需要花费 500 金币
},
];
// LevelManager 中的检查逻辑
async function isLevelUnlocked(levelId: number, userProgress: UserData): Promise<boolean> {
const rule = LEVEL_UNLOCK_RULES.find(r => r.levelId === levelId);
if (!rule) return false;
switch (rule.type) {
case 'default':
return true;
case 'previousLevel':
const prevProgress = await saveManager.loadLevelProgress(rule.previousLevelId!);
return prevProgress?.bestStars! >= (rule.requiredStars ?? 1);
case 'purchase':
return userProgress.coins >= (rule.cost ?? 0);
case 'event':
return await events.has(rule.eventName!);
default:
return false;
}
}每个 Scene 必须实现完整的生命周期:
interface IScene {
create(): Promise<void>; // 初始化(资源加载)
enter(): Promise<void>; // 进入场景(动画开始)
update(deltaTime: number): void; // 每帧更新
exit(): Promise<void>; // 离开场景(清理)
destroy(): void; // 销毁(释放内存)
}
class HomeScene implements IScene {
async create(): Promise<void> {
// 加载资源、初始化 UI
}
async enter(): Promise<void> {
// 播放进场动画
}
update(deltaTime: number): void {
// 每帧更新逻辑
}
async exit(): Promise<void> {
// 播放退出动画
}
destroy(): void {
// 释放纹理、移除事件监听等
}
}npm install
npm run dev # 打开浏览器即可游玩浏览器模式下:
PlatformAdapter自动回退到 DOM canvas + localStorage- 广告/音频为安全空实现
- 游戏逻辑完全一致
npm run build # 产物在 dist/微信小游戏:
- 用「微信开发者工具」导入
dist/目录 - 选「小游戏」编译类型
- 填入小游戏 AppID 或选「使用测试号」
- 上传审核
// ❌ 禁止 any
function process(data: any) { }
// ❌ 禁止 as any
const x = data as any;
// ❌ 禁止 unknown 而不检查
function handle(value: unknown) {
value.prop(); // 错误:未检查
}// ✅ 完整的类型定义
interface LevelConfig {
levelId: number;
bottles: BottleConfig[];
starCriteria: LevelStarCriteria;
}
// ✅ 使用 unknown 时必须检查
function handle(value: unknown): void {
if (typeof value === 'object' && value !== null) {
// 现在可以安全使用
}
}使用 Conventional Commits:
git commit -m "feat: 添加关卡生成算法"
git commit -m "fix: 修复倒水动画闪烁问题"
git commit -m "refactor: 优化 Bottle 类职责"
git commit -m "docs: 补充 Manager 接口说明"任何新增功能,必须遵循:
1️⃣ 阅读 README(理解架构)
↓
2️⃣ 检查现有 Manager(是否可扩展)
↓
3️⃣ 确认目录位置(遵循目录规范)
↓
4️⃣ 设计接口(定义类型 + 方法)
↓
5️⃣ 配置驱动(参数 JSON 化)
↓
6️⃣ 事件驱动(低耦合设计)
↓
7️⃣ 开始编码(注释 + 类型 + 测试)
不得自行改变项目架构。
MIT