# mm Event 事件系统

## 一、Event 的定位

Event 是 mm_os 的**请求拦截/分发层**，通过 `target` 匹配 URL 路径（支持 `*` 通配符），在请求生命周期的各阶段执行拦截逻辑。

### Event 与 API 的关系

```
请求 → [Event 拦截] → [API 业务处理] → 响应
       路由/分发层          业务层
       target + *匹配      具体路由路径
       阶段拦截             业务逻辑
```

| | Event | API |
|---|---|---|
| **定位** | 路由拦截/分发层 | 业务处理层 |
| **匹配方式** | `target` + `*` 通配符 | `api.json` 具体路径 |
| **典型用途** | 权限校验、参数预处理、请求分发、后处理 | CRUD、业务逻辑、数据返回 |
| **委托关系** | Event 通过 `$.admin.api().run(ctx, db)` 委托给 API | API 被 Event 或直接路由调用 |

> **核心原则：** Event 本身不处理具体业务，负责「拦截 → 判断 → 分发」。实际业务由 API 完成。

## 二、两个层级

| | App 级 Event | Plugin 级 Event |
|---|---|---|
| **路径** | `app/{模块名}/event_api/{client\|web\|manage}/{事件名}/` | `app/{模块名}/plugin/{插件名}/event_api/{事件名}/` |
| **event.json** | 单个对象 | 数组格式（可含多个阶段配置） |
| **脚本文件** | `main.js` | `index.js` / `before.js` / `after.js` |
| **导出方式** | `module.exports = { _init, main }` | `exports.main = main`（与 API 脚本一致） |
| **适用场景** | 全局拦截（如拦截所有 `/api/*` 请求） | 插件内部预处理/后处理 |
| **参考示例** | `app/sys/event_api/web/` | `app/sys/plugin/tencent/event_api/change_url/` |

### 如何选择

- **需要拦截整个 App 的所有请求** → App 级 Event
- **只需要对某个插件的请求做预处理/后处理** → Plugin 级 Event
- Plugin 级更轻量，不污染全局事件链

## 三、五个触发阶段（stage）

请求生命周期按顺序经过 5 个阶段：

```
请求进入 → before → check → main → render → after → 响应返回
```

| stage | 触发时机 | 典型用途 | 返回值含义 |
|:------|:---------|:---------|:-----------|
| `before` | 请求进入，业务处理前 | 权限校验、Token验证、参数预处理、URL转换 | 返回非 null 则中断请求 |
| `check` | 参数校验阶段 | 安装检测、环境验证、数据完整性检查 | 返回非 null 则中断请求 |
| `main` | 核心业务阶段 | 请求分发（通过 `api.run` 委托给 API 管理器） | 正常业务返回值 |
| `render` | 模板渲染前 | 数据准备、模型注入到模板上下文 | 可修改渲染数据 |
| `after` | 响应返回前 | 后处理、数据转换、URL签名、响应头修改 | 可修改响应内容 |

> 同一 target 可注册多个 Event，按 `sort` 字段排序依次执行。`end: true` 终止事件链，后续 Event 不再执行。

## 四、event.json 配置

### App 级 — 单对象格式

```json
{
    "target": "/api/*",
    "name": "sys_api",
    "title": "系统API事件",
    "description": "拦截API请求并分发",
    "stage": "main",
    "main": "./main.js",
    "sort": 110,
    "end": true
}
```

### Plugin 级 — 数组格式

同一文件定义多个阶段的处理器：

```json
[
    {
        "target": "/api/*",
        "name": "change_url_before",
        "title": "URL预处理",
        "stage": "before",
        "main": "./before.js",
        "sort": 10
    },
    {
        "target": "/api/*",
        "name": "change_url_main",
        "title": "核心分发",
        "stage": "main",
        "main": "./index.js",
        "sort": 20,
        "end": true
    },
    {
        "target": "/api/*",
        "name": "change_url_after",
        "title": "后处理",
        "stage": "after",
        "main": "./after.js",
        "sort": 30
    }
]
```

### 字段说明

| 字段 | 类型 | 必填 | 说明 |
|:-----|:-----|:-----|:------|
| `target` | string | ✅ | URL 匹配模式，`*` 通配符。如 `/api/*`、`/upload*` |
| `name` | string | ✅ | 事件唯一标识 |
| `title` | string | ✅ | 事件中文标题 |
| `description` | string | | 事件描述 |
| `stage` | string | ✅ | 触发阶段：`before` / `check` / `main` / `render` / `after` |
| `main` | string | ✅ | 处理器脚本路径（相对 event.json 所在目录） |
| `sort` | number | | 执行顺序，值越小越先执行，默认 `10` |
| `end` | boolean | | 是否结束事件链，`true` 表示后续事件不再执行 |
| `state` | number | | 启用状态：`1`启用 / `0`禁用，默认 `1` |

## 五、脚本编写

### App 级 — module.exports 导出

```javascript
// main.js
var api = $.admin.api('分组名', '分组标题');

module.exports = {
    async _init() {
        // 初始化：注册 API 路由
        await api.call('update', 'app/');
    },

    async main(ctx, db) {
        // 注入 sys 数据库连接
        $.push(db, $.admin.sql('sys').db(), true);
        // 委托给 API 管理器处理
        return await api.run(ctx, db);
    }
};
```

### Plugin 级 — exports.main 导出

```javascript
// index.js — stage: main（核心分发）
var api = $.admin.api('分组名', '分组标题');

async function main(ctx, db) {
    $.push(db, $.admin.sql('sys').db(), true);
    return await api.run(ctx, db);
}
exports.main = main;
```

```javascript
// before.js — stage: before（前置处理）
async function main(ctx, db) {
    var { query } = ctx.request;
    if (!query.token) {
        return $.ret.error(401, '未授权');  // 返回错误中断请求
    }
    return null;  // 返回 null 继续后续流程
}
exports.main = main;
```

```javascript
// after.js — stage: after（后置处理）
async function main(ctx, db) {
    // 后处理、数据转换、URL签名等
    return null;
}
exports.main = main;
```

### 返回值约定

| 返回值 | 含义 |
|:-------|:------|
| `null` / `undefined` | ✅ 继续后续流程 |
| `$.ret.error()` 或非空对象 | ❌ 中断请求，返回错误 |
| 其他值 | 传递给下一个 Event 或作为最终响应 |

## 六、Event 委托 API 的典型模式

```javascript
// 1. 在 _init 中注册 API 路由
var api = $.admin.api('分组名', '分组标题');
await api.call('update', 'app/');

// 2. 在 main 中注入数据库并委托
$.push(db, $.admin.sql('sys').db(), true);
return await api.run(ctx, db);
```

`$.admin.api()` 创建 API 管理器，`api.run()` 自动匹配 `api.json` 中注册的路由并执行对应的 `index.js`。这样 Event 不需要关心具体有哪些 API，只需要负责拦截和分发。

## 七、常见组合模式

| 模式 | 配置 | 适用场景 |
|:-----|:-----|:---------|
| **纯拦截** | `before` + 返回错误 | Token校验、黑名单拦截 |
| **纯分发** | `main` + `api.run()` | 统一 API 入口 |
| **预处理+分发** | `before` + `main` | URL转换后分发 |
| **分发+后处理** | `main` + `after` | 响应签名、数据脱敏 |
| **全流程** | `before` + `main` + `after` | 完整请求生命周期管控 |
