@layoutkit/bree-core 1.0.10 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +358 -26
- package/dist/index.js +665 -327
- package/dist/index.js.map +1 -1
- package/package.json +33 -33
package/README.md
CHANGED
|
@@ -1,26 +1,358 @@
|
|
|
1
|
-
# bree-core
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
1
|
+
# @layoutkit/bree-core
|
|
2
|
+
|
|
3
|
+
可复用的 Bree 任务调度 + Express 服务核心包,内置数据库、Redis、日志、认证、Excel、邮件等开箱即用能力。
|
|
4
|
+
|
|
5
|
+
基于 **worker_threads**:任务在独立线程中执行,主进程负责调度与状态回写。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @layoutkit/bree-core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
需自行安装 peer 依赖(`bree`、`express`、`knex`、`mssql`、`redis`、`winston`、`winston-daily-rotate-file`、`exceljs`、`nodemailer`、`dotenv`)。
|
|
14
|
+
|
|
15
|
+
## 目录结构约定
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
项目根/
|
|
19
|
+
├── app.js # 主进程:注册 hooks、scheduler、useServer
|
|
20
|
+
├── jobs/ # 任务目录(也支持 src/jobs 或 dist/jobs)
|
|
21
|
+
│ └── test.js # 任务文件:useHandler().run(业务函数)
|
|
22
|
+
├── routes/ # 路由目录(useServer 自动扫描,也支持 src/routes)
|
|
23
|
+
│ └── user.js
|
|
24
|
+
└── .env # 环境变量(数据库、Redis、认证等)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 快速开始
|
|
28
|
+
|
|
29
|
+
### 1. 主进程 `app.js` —— 注册 hooks 并启动调度器
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import dotenv from 'dotenv'
|
|
33
|
+
dotenv.config()
|
|
34
|
+
|
|
35
|
+
import { scheduler, hooks, useServer } from '@layoutkit/bree-core'
|
|
36
|
+
|
|
37
|
+
// ===== hooks:状态回写(在主进程执行,直接赋值即生效)=====
|
|
38
|
+
|
|
39
|
+
// 任务列表:从数据库读取并注册到调度器
|
|
40
|
+
hooks.registerLoader = async (db, logger) => {
|
|
41
|
+
const jobs = await db('My_Jobs').where('Enabled', true)
|
|
42
|
+
for (const job of jobs) {
|
|
43
|
+
await scheduler.create({
|
|
44
|
+
name: job.Name, // 对应 jobs/<Name>.js
|
|
45
|
+
interval: job.Interval, // 如 '5 minutes' / '*/10 * * * * *'
|
|
46
|
+
workerData: { id: job.ID }, // 回写时用业务主键 ID
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 任务成功:worker 通过 postMessage 传回结果,主进程统一回写
|
|
52
|
+
hooks.registerComplete = async (db, { id, result }) => {
|
|
53
|
+
await db('My_Jobs').where('ID', id).update({ Status: 'done', Result: JSON.stringify(result) })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 任务失败:bree errorHandler 捕获后统一回写
|
|
57
|
+
hooks.registerError = async (db, { id, error }) => {
|
|
58
|
+
await db('My_Jobs').where('ID', id).update({ Status: 'error', Error: error })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ===== 启动调度器 =====
|
|
62
|
+
await scheduler.register()
|
|
63
|
+
|
|
64
|
+
// ===== 启动 HTTP 服务(可选)=====
|
|
65
|
+
const { app } = useServer({ port: 3000, auth: false })
|
|
66
|
+
await app.start()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 2. 任务文件 `jobs/test.js` —— worker 线程内
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
import { useHandler } from '@layoutkit/bree-core'
|
|
73
|
+
|
|
74
|
+
const { run, getIsCancelled } = useHandler()
|
|
75
|
+
|
|
76
|
+
const test = async ({ logger, redis, db }) => {
|
|
77
|
+
logger.info('任务开始')
|
|
78
|
+
|
|
79
|
+
// 业务逻辑(可访问注入的 logger / redis / db)
|
|
80
|
+
const count = await db('HI_User').count()
|
|
81
|
+
await redis.set('lastCount', count, 3600)
|
|
82
|
+
|
|
83
|
+
// 支持优雅取消:scheduler.stop('test') 后提前退出
|
|
84
|
+
if (getIsCancelled()) return { cancelled: true }
|
|
85
|
+
|
|
86
|
+
return { ok: true, count } // 会通过 postMessage 回传给主进程
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
run(test)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 3. 启动
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
node app.js
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## 环境变量
|
|
99
|
+
|
|
100
|
+
| 变量 | 用途 | 示例 |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| `DB_CONNECTION_STRING` | SQL Server 连接串 | `user=sa;password=123;server=127.0.0.1;database=mydb` |
|
|
103
|
+
| `DB_AES_KEY` | 可选,连接串为 AES 密文时用于解密 | `16/24/32字节密钥` |
|
|
104
|
+
| `REDIS_HOST` / `REDIS_PORT` | Redis 地址 | `127.0.0.1` / `6379` |
|
|
105
|
+
| `REDIS_USERNAME` / `REDIS_PASSWORD` | Redis 账号密码(可选) | |
|
|
106
|
+
| `REDIS_DB` | Redis 库号(默认 0) | `0` |
|
|
107
|
+
| `MAIL_HOST` / `MAIL_PORT` / `MAIL_ACCOUNT` / `MAIL_PASSWORD` / `MAIL_USERNAME` | SMTP 配置(MAIL_PASSWORD 为授权码) | |
|
|
108
|
+
| `API_SECRET_KEY` | 认证 AES 密钥 | 16/24/32字节 |
|
|
109
|
+
| `API_TOKEN` | 认证明文 token | |
|
|
110
|
+
| `API_SECRET_KEY_NAME` | 认证请求头名 | `x-api-key` |
|
|
111
|
+
| `ALLOW_IPS` | IP 白名单(逗号分隔) | `127.0.0.1,192.168.1.1` |
|
|
112
|
+
| `LOG_DIR` | 日志根目录(默认项目下 `logs`) | `/var/log` |
|
|
113
|
+
| `BREE_JOBS_PATH` | 任务目录(不设则自动找 `src/jobs` / `jobs` / `dist/jobs`) | `./jobs` |
|
|
114
|
+
|
|
115
|
+
## 任务调度
|
|
116
|
+
|
|
117
|
+
### 数据流
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
主进程 worker 线程 (jobs/<name>.js)
|
|
121
|
+
─────── ────────────────────────────
|
|
122
|
+
scheduler.register()
|
|
123
|
+
└─ hooks.registerLoader(db, logger) ┌─────────────────────────┐
|
|
124
|
+
└─ scheduler.create(job) │ useHandler().run(业务) │
|
|
125
|
+
scheduler.start('test') / run('test') ──▶ │ ├─ worker({logger, │
|
|
126
|
+
│ │ │ redis, db}) │
|
|
127
|
+
▼ │ └─ postMessage(done) │
|
|
128
|
+
worker created ─────────────────────────▶└─────────────────────────┘
|
|
129
|
+
│
|
|
130
|
+
worker message { type:'done', data }
|
|
131
|
+
└─ hooks.registerComplete(db, { id, result })
|
|
132
|
+
|
|
133
|
+
worker 抛错 ──▶ bree errorHandler
|
|
134
|
+
└─ hooks.registerError(db, { id, error })
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### hooks —— 状态回写钩子
|
|
138
|
+
|
|
139
|
+
hooks 是主进程共享对象,包含三个钩子,**直接赋值即生效**:
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
hooks.registerLoader = async (db, logger) => { /* 读任务列表并 scheduler.create */ }
|
|
143
|
+
hooks.registerComplete = async (db, { id, result }) => { /* 任务成功回写 */ }
|
|
144
|
+
hooks.registerError = async (db, { id, error }) => { /* 任务失败回写 */ }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
- 不赋值时使用默认空实现(不会崩溃,只提示一次)。
|
|
148
|
+
- 回写参数:`id` 为 `bree.add()` 时写入的 `workerData.id`(在 `registerLoader` 里通过 `workerData: { id: 业务主键 }` 自定义),`result` 为 worker 返回的数据,`error` 为异常堆栈。
|
|
149
|
+
- **注意**:worker 线程内存独立,主进程的 hooks 单例在 worker 里不可见,因此回写一律发生在主进程(成功走 `postMessage`,失败走 `errorHandler`),业务函数里无需也不应注入回写钩子。
|
|
150
|
+
|
|
151
|
+
### scheduler —— 调度器(主进程)
|
|
152
|
+
|
|
153
|
+
| 方法 | 说明 |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| `register()` | 初始化数据库 + Bree 实例,并调用 `hooks.registerLoader` 加载任务列表 |
|
|
156
|
+
| `create(job)` | 注册任务(同名已存在则跳过)。`job` 为 Bree job 配置,如 `{ name, interval, workerData }` |
|
|
157
|
+
| `reset(job, startStatus?)` | 停止并替换同名任务,可选是否立即启动 |
|
|
158
|
+
| `remove(name)` | 停止并移除任务 |
|
|
159
|
+
| `start(name)` | 启动(后台周期性运行) |
|
|
160
|
+
| `stop(name)` | 停止(向 worker 发送 cancel 消息,触发优雅取消) |
|
|
161
|
+
| `run(name)` | 立即执行一次 |
|
|
162
|
+
| `getBree()` | 获取底层 Bree 实例 |
|
|
163
|
+
|
|
164
|
+
任务文件按 `name` 对应 `jobs/<name>.js`。定时支持 Bree 的 `interval` 语法(如 `'5 minutes'`)或 cron 表达式(`hasSeconds` 已开启)。
|
|
165
|
+
|
|
166
|
+
### useHandler —— 任务执行(worker 内)
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
import { useHandler } from '@layoutkit/bree-core'
|
|
170
|
+
|
|
171
|
+
const { run, getIsCancelled } = useHandler()
|
|
172
|
+
|
|
173
|
+
const myJob = async ({ logger, redis, db }) => { /* 业务逻辑 */ }
|
|
174
|
+
run(myJob)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
- 业务函数接收 `{ logger, redis, db }`:日志、Redis 单例、knex 数据库实例均已初始化。
|
|
178
|
+
- **成功**:`run` 自动执行 `postMessage({ type: 'done', data: result })`,主进程调用 `hooks.registerComplete`。业务函数直接 `return` 结果即可,无需手动回写。
|
|
179
|
+
- **失败**:worker 抛错由 bree `errorHandler` 捕获,主进程调用 `hooks.registerError`。
|
|
180
|
+
- **取消**:主进程 `scheduler.stop(name)` 会向 worker 发送 `cancel` 消息,`useHandler` 内部设置取消标志;业务函数内用 `getIsCancelled()` 检查并提前退出。
|
|
181
|
+
|
|
182
|
+
## Express 服务
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
import { useServer, route } from '@layoutkit/bree-core'
|
|
186
|
+
|
|
187
|
+
const { app } = useServer({
|
|
188
|
+
port: 3000,
|
|
189
|
+
auth: true, // true=内置认证;也可传自定义中间件函数/数组;false=关闭
|
|
190
|
+
routesDir: ['routes'], // 自动扫描路由目录,默认 ['routes', 'src/routes']
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
await app.start()
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### 路由 —— 单函数接口风格(定义即自动注册)
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
import { route } from '@layoutkit/bree-core'
|
|
200
|
+
|
|
201
|
+
// handler 签名:async (body, req, res) => result
|
|
202
|
+
// body = { ...params, ...query, ...body },解构即用
|
|
203
|
+
route.post('/test', ({ name }) => {
|
|
204
|
+
return { code: 0, message: 'success', data: { name } } // 完整响应体原样返回
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
route.get('/user/:id', async ({ id }) => ({ id })) // 普通数据自动 res.success 包装
|
|
208
|
+
|
|
209
|
+
// 分组前缀
|
|
210
|
+
const api = route.parentRoute('/task')
|
|
211
|
+
api.get('/list', async () => []) // GET /task/list
|
|
212
|
+
|
|
213
|
+
// 支持中间件(如 multer 上传)
|
|
214
|
+
route.post('/upload', upload.single('file'), async (body, req) => {
|
|
215
|
+
return { code: 0, message: '上传成功', data: { filename: req.file.filename } }
|
|
216
|
+
})
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
- 方法:`route.get / route.post / route.put / route.delete / route.patch`,裸 `route(path, ...)` 默认 POST。
|
|
220
|
+
- 中间件:单个函数或函数数组,放在 handler 之前。
|
|
221
|
+
- 返回含 `code` 的对象原样输出,其余自动包装为 `res.success`;抛错进入全局错误处理。
|
|
222
|
+
|
|
223
|
+
### useServer 选项
|
|
224
|
+
|
|
225
|
+
| 选项 | 默认 | 说明 |
|
|
226
|
+
| --- | --- | --- |
|
|
227
|
+
| `port` | `3000` | 监听端口 |
|
|
228
|
+
| `host` | `0.0.0.0` | 监听地址 |
|
|
229
|
+
| `logger` | `createLogger({ name:'server', split:true })` | 自定义 logger |
|
|
230
|
+
| `jsonLimit` / `urlencodedLimit` | `10mb` | body 大小限制 |
|
|
231
|
+
| `cors` | `true` | 跨域;可传 `origin` 字符串或 `{ origin, methods, headers }` |
|
|
232
|
+
| `staticDir` | `null` | 静态资源目录(可传数组) |
|
|
233
|
+
| `auth` | `false` | `true`=内置认证 / 函数或数组=自定义中间件 |
|
|
234
|
+
| `health` | `true` | 启用 `GET /health` |
|
|
235
|
+
| `trustProxy` | `true` | 信任代理(影响 `req.ip`) |
|
|
236
|
+
| `routes` | `null` | 函数 `(app) => {}` 或路由配置数组 |
|
|
237
|
+
| `routesDir` | `['routes','src/routes']` | 路由文件目录,`start()` 时递归扫描自动加载;`false` 关闭 |
|
|
238
|
+
|
|
239
|
+
内置能力:请求日志(含耗时/状态码/IP)、CORS、`res.success(data, message)` / `res.fail(message, code)` 统一响应、404 与全局错误处理、健康检查。
|
|
240
|
+
|
|
241
|
+
### 内置认证 auth
|
|
242
|
+
|
|
243
|
+
```js
|
|
244
|
+
useServer({ auth: true })
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
- 从 `req.headers[API_SECRET_KEY_NAME]` 取密钥,值需为 **AES 密文**(用 `API_SECRET_KEY` 解密后等于 `API_TOKEN` 才通过);
|
|
248
|
+
- 校验请求 IP 是否在 `ALLOW_IPS` 白名单内;
|
|
249
|
+
- 失败统一返回 HTTP 200 + `{ code: -1, message }`。
|
|
250
|
+
|
|
251
|
+
## 日志 createLogger
|
|
252
|
+
|
|
253
|
+
```js
|
|
254
|
+
import { createLogger } from '@layoutkit/bree-core'
|
|
255
|
+
|
|
256
|
+
const logger = createLogger({
|
|
257
|
+
name: 'weixinapi', // 文件名前缀 + 默认子目录名
|
|
258
|
+
level: 'info', // error | warn | info | debug(默认 info,debug 会被过滤)
|
|
259
|
+
split: true, // 按 debug/info/warn/error 拆四个文件;可传数组如 ['info','error']
|
|
260
|
+
// maxSize: '10m', maxFiles: '10d', dirname: '...', console: false, file: true
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
logger.error('...') // 输出到 stderr
|
|
264
|
+
logger.info('...') // 输出到 stdout
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
- 文件:`logs/<dirname ?? name>/<name>-%DATE%.log`(按天滚动);设置 `LOG_DIR` 后根目录变为 `<LOG_DIR>/logs`。
|
|
268
|
+
- 默认级别 `info`,想看到 `debug` 需显式 `level: 'debug'`。
|
|
269
|
+
- 默认 `maxSize: '10m'`、`maxFiles: '10d'`。
|
|
270
|
+
|
|
271
|
+
## 工具模块
|
|
272
|
+
|
|
273
|
+
### redis
|
|
274
|
+
|
|
275
|
+
```js
|
|
276
|
+
import { redis } from '@layoutkit/bree-core'
|
|
277
|
+
|
|
278
|
+
await redis.set('key', { a: 1 }, 3600) // 对象自动 JSON 序列化,可选过期秒
|
|
279
|
+
const v = await redis.get('key') // 自动尝试 JSON.parse
|
|
280
|
+
await redis.del('key')
|
|
281
|
+
await redis.hSet('hash', 'field', { x: 1 })
|
|
282
|
+
await redis.lPush('list', item) // 列表元素 JSON 序列化
|
|
283
|
+
await redis.close() // 关闭连接
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
方法:`set` / `get` / `del` / `exists` / `expire` / `ttl` / `incr` / `decr` / `hSet` / `hGet` / `hGetAll` / `lPush` / `rPush` / `lPop` / `rPop` / `close`。
|
|
287
|
+
|
|
288
|
+
### database(knex + mssql)
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
import { database } from '@layoutkit/bree-core'
|
|
292
|
+
|
|
293
|
+
const db = await database.init() // 默认读 DB_CONNECTION_STRING
|
|
294
|
+
const db2 = await database.init('user=..;password=..;server=..;database=..') // 可传参覆盖
|
|
295
|
+
const rows = await database.get()('users').select('*')
|
|
296
|
+
await database.close()
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
- 连接串支持 `key=value;key=value` 格式;配置 `DB_AES_KEY` 时自动尝试 AES 解密(明文则原样使用)。
|
|
300
|
+
- `init()` 会先释放旧连接并 `SELECT 1` 验证真实连通。
|
|
301
|
+
|
|
302
|
+
### aes(AES/ECB/PKCS7,兼容 Java)
|
|
303
|
+
|
|
304
|
+
```js
|
|
305
|
+
import { aes } from '@layoutkit/bree-core'
|
|
306
|
+
|
|
307
|
+
const cipher = aes.encrypt('明文', '16字节密钥') // 返回 Buffer
|
|
308
|
+
const plain = aes.decrypt(cipher, '16字节密钥') // 返回 Buffer
|
|
309
|
+
const str = aes.decryptString('Base64密文', '16字节密钥') // 密文字符串 → 明文
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### excel
|
|
313
|
+
|
|
314
|
+
```js
|
|
315
|
+
import { excel } from '@layoutkit/bree-core'
|
|
316
|
+
|
|
317
|
+
// 统一入口:file(落盘) | buffer(邮件用) | stream(10w+ 大数据流式)
|
|
318
|
+
const filePath = await excel.exportExcel({
|
|
319
|
+
mode: 'file',
|
|
320
|
+
columns: [{ header: 'ID', key: 'id', width: 10 }],
|
|
321
|
+
data: [{ id: 1 }],
|
|
322
|
+
fileName: 'report',
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
excel.setFolder('exports') // 可选:文件输出目录(相对 cwd)
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### mail
|
|
329
|
+
|
|
330
|
+
```js
|
|
331
|
+
import { mail } from '@layoutkit/bree-core'
|
|
332
|
+
|
|
333
|
+
await mail.init() // 读取 MAIL_* 环境变量
|
|
334
|
+
await mail.send(
|
|
335
|
+
'主题',
|
|
336
|
+
'a@x.com,b@x.com', // 收件人(逗号分隔)
|
|
337
|
+
'cc@x.com', // 抄送(可选)
|
|
338
|
+
'正文',
|
|
339
|
+
['/path/to/file.xlsx'] // 附件(可选,自动截取文件名)
|
|
340
|
+
)
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## API 导出总览
|
|
344
|
+
|
|
345
|
+
| 导出 | 说明 |
|
|
346
|
+
| --- | --- |
|
|
347
|
+
| `scheduler` | 调度器(主进程) |
|
|
348
|
+
| `hooks` | 状态回写钩子(主进程共享对象) |
|
|
349
|
+
| `useHandler` | 任务执行器(worker 内) |
|
|
350
|
+
| `useServer` | Express 服务封装 |
|
|
351
|
+
| `route` | 单函数风格路由 |
|
|
352
|
+
| `auth` | 内置认证中间件 |
|
|
353
|
+
| `createLogger` | 日志工厂 |
|
|
354
|
+
| `database` | knex/mssql 数据库单例 |
|
|
355
|
+
| `redis` | Redis 单例 |
|
|
356
|
+
| `aes` | AES 加解密工具 |
|
|
357
|
+
| `excel` | Excel 导出工具 |
|
|
358
|
+
| `mail` | 邮件发送工具 |
|