befly 0.1.26

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/main.js ADDED
@@ -0,0 +1,410 @@
1
+ import path from 'node:path';
2
+ import { Env } from './config/env.js';
3
+
4
+ // 工具函数
5
+ import { Api } from './utils/api.js';
6
+ import { Logger } from './utils/logger.js';
7
+ import { Jwt } from './utils/jwt.js';
8
+ import { validator } from './utils/validate.js';
9
+ import { Crypto2 } from './utils/crypto.js';
10
+ import { XMLParser } from './libs/xml/XMLParser.js';
11
+ import { isEmptyObject, isType, pickFields, sortPlugins, RYes, RNo, filename2, dirname2 } from './utils/util.js';
12
+
13
+ class BunPii {
14
+ constructor(options = {}) {
15
+ this.apiRoutes = new Map();
16
+ this.pluginLists = [];
17
+ this.appContext = {};
18
+ this.appOptions = options;
19
+ }
20
+
21
+ async initCheck() {
22
+ try {
23
+ const checksDir = path.join(dirname2(import.meta.url), 'checks');
24
+ const glob = new Bun.Glob('*.js');
25
+
26
+ // 统计信息
27
+ let totalChecks = 0;
28
+ let passedChecks = 0;
29
+ let failedChecks = 0;
30
+
31
+ // 扫描并执行检查函数
32
+ for await (const file of glob.scan({
33
+ cwd: checksDir,
34
+ onlyFiles: true,
35
+ absolute: true
36
+ })) {
37
+ const fileName = path.basename(file);
38
+ if (fileName.startsWith('_')) continue; // 跳过以下划线开头的文件
39
+
40
+ try {
41
+ totalChecks++;
42
+
43
+ // 导入检查模块
44
+ const check = await import(file);
45
+
46
+ // 执行默认导出的函数
47
+ if (typeof check.default === 'function') {
48
+ const checkResult = await check.default(this.appContext);
49
+ if (checkResult === true) {
50
+ passedChecks++;
51
+ } else {
52
+ Logger.error(`检查未通过: ${fileName}`);
53
+ failedChecks++;
54
+ }
55
+ } else {
56
+ Logger.warn(`文件 ${fileName} 未导出默认函数`);
57
+ failedChecks++;
58
+ }
59
+ } catch (error) {
60
+ Logger.error({
61
+ msg: `检查失败 ${fileName}`,
62
+ error: error.message,
63
+ stack: error.stack
64
+ });
65
+ failedChecks++;
66
+ }
67
+ }
68
+
69
+ // 输出检查结果统计
70
+ Logger.info(`总检查数: ${totalChecks}, 通过: ${passedChecks}, 失败: ${failedChecks}`);
71
+
72
+ if (failedChecks > 0) {
73
+ process.exit();
74
+ } else if (totalChecks > 0) {
75
+ Logger.info(`所有系统检查通过!`);
76
+ } else {
77
+ Logger.info(`未执行任何检查`);
78
+ }
79
+ } catch (error) {
80
+ Logger.error({
81
+ msg: '加载接口时发生错误',
82
+ error: error.message,
83
+ stack: error.stack
84
+ });
85
+ process.exit();
86
+ }
87
+ }
88
+
89
+ async loadPlugins() {
90
+ try {
91
+ const glob = new Bun.Glob('*.js');
92
+ const corePlugins = [];
93
+ const userPlugins = [];
94
+ const loadedPluginNames = new Set(); // 用于跟踪已加载的插件名称
95
+
96
+ // 扫描指定目录
97
+ for await (const file of glob.scan({
98
+ cwd: path.join(dirname2(import.meta.url), 'plugins'),
99
+ onlyFiles: true,
100
+ absolute: true
101
+ })) {
102
+ const fileName = path.basename(file, '.js');
103
+ if (fileName.startsWith('_')) continue;
104
+ const plugin = await import(file);
105
+ const pluginInstance = plugin.default;
106
+ pluginInstance.pluginName = fileName;
107
+ corePlugins.push(pluginInstance);
108
+ loadedPluginNames.add(fileName); // 记录已加载的核心插件名称
109
+ }
110
+
111
+ const sortedCorePlugins = sortPlugins(corePlugins);
112
+ if (sortedCorePlugins === false) {
113
+ Logger.error(`插件依赖关系错误,请检查插件的 after 属性`);
114
+ process.exit();
115
+ }
116
+
117
+ for (const plugin of sortedCorePlugins) {
118
+ try {
119
+ this.pluginLists.push(plugin);
120
+ this.appContext[plugin.pluginName] = typeof plugin?.onInit === 'function' ? await plugin?.onInit(this.appContext) : {};
121
+ } catch (error) {
122
+ Logger.warn(`插件 ${plugin.pluginName} 初始化失败:`, error.message);
123
+ }
124
+ }
125
+
126
+ // 扫描指定目录
127
+ for await (const file of glob.scan({
128
+ cwd: path.join(process.cwd(), 'plugins'),
129
+ onlyFiles: true,
130
+ absolute: true
131
+ })) {
132
+ const fileName = path.basename(file, '.js');
133
+ if (fileName.startsWith('_')) continue;
134
+
135
+ // 检查是否已经加载了同名的核心插件
136
+ if (loadedPluginNames.has(fileName)) {
137
+ Logger.info(`跳过用户插件 ${fileName},因为同名的核心插件已存在`);
138
+ continue;
139
+ }
140
+
141
+ const plugin = await import(file);
142
+ const pluginInstance = plugin.default;
143
+ pluginInstance.pluginName = fileName;
144
+ userPlugins.push(pluginInstance);
145
+ }
146
+
147
+ const sortedUserPlugins = sortPlugins(userPlugins);
148
+ if (sortedUserPlugins === false) {
149
+ Logger.error(`插件依赖关系错误,请检查插件的 after 属性`);
150
+ process.exit();
151
+ }
152
+
153
+ for (const plugin of sortedUserPlugins) {
154
+ try {
155
+ this.pluginLists.push(plugin);
156
+ this.appContext[plugin.pluginName] = typeof plugin?.onInit === 'function' ? await plugin?.onInit(this.appContext) : {};
157
+ } catch (error) {
158
+ Logger.warn(`插件 ${plugin.pluginName} 初始化失败:`, error.message);
159
+ }
160
+ }
161
+ } catch (error) {
162
+ Logger.error({
163
+ msg: '加载插件时发生错误',
164
+ error: error.message,
165
+ stack: error.stack
166
+ });
167
+ }
168
+ }
169
+ async loadApis(dirName) {
170
+ try {
171
+ const coreApisDir = path.join(dirname2(import.meta.url), 'apis');
172
+ const userApisDir = path.join(process.cwd(), 'apis');
173
+ const glob = new Bun.Glob('**/*.js');
174
+ const apiDir = dirName === 'core' ? coreApisDir : userApisDir;
175
+ // 扫描指定目录
176
+ for await (const file of glob.scan({
177
+ cwd: apiDir,
178
+ onlyFiles: true,
179
+ absolute: true
180
+ })) {
181
+ const fileName = path.basename(file, '.js');
182
+ const apiPath = path.relative(apiDir, file).replace(/\.js$/, '').replace(/\\/g, '/');
183
+ if (apiPath.indexOf('_') !== -1) continue;
184
+ const api = await import(file);
185
+ const apiInstance = api.default;
186
+ if (isType(api.name, 'string') === false || api.name.trim() === '') {
187
+ throw new Error(`接口 ${apiPath} 的 name 属性必须是非空字符串`);
188
+ }
189
+ if (api.auth !== false && api.auth !== true && Array.isArray(api.auth) === false) {
190
+ throw new Error(`接口 ${apiPath} 的 auth 属性必须是布尔值或字符串数组`);
191
+ }
192
+ if (isType(api.fields, 'object') === false) {
193
+ throw new Error(`接口 ${apiPath} 的 fields 属性必须是对象`);
194
+ }
195
+ if (isType(api.required, 'array') === false) {
196
+ throw new Error(`接口 ${apiPath} 的 required 属性必须是数组`);
197
+ }
198
+ // 数组的每一项都必须是字符串
199
+ if (api.required.some((item) => isType(item, 'string') === false)) {
200
+ throw new Error(`接口 ${apiPath} 的 required 属性必须是字符串数组`);
201
+ }
202
+ if (isType(api.handler, 'function') === false) {
203
+ throw new Error(`接口 ${apiPath} 的 handler 属性必须是函数`);
204
+ }
205
+ apiInstance.route = `${apiInstance.method.toUpperCase()}/api/${dirName}/${apiPath}`;
206
+ this.apiRoutes.set(apiInstance.route, apiInstance);
207
+ }
208
+ } catch (error) {
209
+ Logger.error({
210
+ msg: '加载接口时发生错误',
211
+ error: error.message,
212
+ stack: error.stack
213
+ });
214
+ }
215
+ }
216
+
217
+ /**
218
+ * 启动服务器
219
+ */
220
+ async listen(callback) {
221
+ await this.initCheck();
222
+ await this.loadPlugins();
223
+ await this.loadApis('core');
224
+ await this.loadApis('app');
225
+
226
+ const server = Bun.serve({
227
+ port: Env.APP_PORT,
228
+ hostname: Env.APP_HOST,
229
+ routes: {
230
+ '/': async (req) => {
231
+ return Response.json({
232
+ code: 0,
233
+ msg: 'BunPii 接口服务已启动',
234
+ data: {
235
+ mode: Env.NODE_ENV
236
+ }
237
+ });
238
+ },
239
+ '/api/*': async (req) => {
240
+ try {
241
+ // 直接返回options请求
242
+ if (req.method === 'OPTIONS') {
243
+ return new Response();
244
+ }
245
+ // 初始化请求数据存储
246
+ const ctx = {
247
+ headers: Object.fromEntries(req.headers.entries()),
248
+ body: {},
249
+ user: {}
250
+ };
251
+
252
+ // 接口处理
253
+ const url = new URL(req.url);
254
+ const apiPath = `${req.method}${url.pathname}`;
255
+
256
+ const api = this.apiRoutes.get(apiPath);
257
+
258
+ // 接口不存在
259
+ if (!api) return Response.json(RNo('接口不存在'));
260
+
261
+ const authHeader = req.headers.get('authorization');
262
+ if (authHeader && authHeader.startsWith('Bearer ')) {
263
+ const token = authHeader.substring(7);
264
+
265
+ try {
266
+ const payload = await Jwt.verify(token);
267
+ ctx.user = payload;
268
+ } catch (error) {
269
+ ctx.user = {};
270
+ }
271
+ } else {
272
+ ctx.user = {};
273
+ }
274
+ // 配置参数
275
+ if (req.method === 'GET') {
276
+ if (isEmptyObject(api.fields) === false) {
277
+ ctx.body = pickFields(Object.fromEntries(url.searchParams), Object.keys(api.fields));
278
+ } else {
279
+ ctx.body = Object.fromEntries(url.searchParams);
280
+ }
281
+ }
282
+ if (req.method === 'POST') {
283
+ try {
284
+ const contentType = req.headers.get('content-type') || '';
285
+
286
+ if (contentType.indexOf('json') !== -1) {
287
+ ctx.body = await req.json();
288
+ } else if (contentType.indexOf('xml') !== -1) {
289
+ const textData = await req.text();
290
+ const xmlData = new XMLParser().parse(textData);
291
+ ctx.body = xmlData?.xml ? xmlData.xml : xmlData;
292
+ } else if (contentType.indexOf('form-data') !== -1) {
293
+ ctx.body = await req.formData();
294
+ } else if (contentType.indexOf('x-www-form-urlencoded') !== -1) {
295
+ const text = await clonedReq.text();
296
+ const formData = new URLSearchParams(text);
297
+ ctx.body = Object.fromEntries(formData);
298
+ } else {
299
+ ctx.body = {};
300
+ }
301
+ if (isEmptyObject(api.fields) === false) {
302
+ ctx.body = pickFields(ctx.body, Object.keys(api.fields));
303
+ }
304
+ } catch (err) {
305
+ Logger.error({
306
+ msg: '处理请求参数时发生错误',
307
+ error: err.message,
308
+ stack: err.stack
309
+ });
310
+
311
+ return Response.json(RNo('无效的请求参数格式'));
312
+ }
313
+ }
314
+
315
+ // 插件钩子
316
+ for await (const plugin of this.pluginLists) {
317
+ try {
318
+ if (typeof plugin?.onGet === 'function') {
319
+ await plugin?.onGet(this.appContext, ctx, req);
320
+ }
321
+ } catch (error) {
322
+ Logger.error({
323
+ msg: '插件处理请求时发生错误',
324
+ error: error.message,
325
+ stack: error.stack
326
+ });
327
+ }
328
+ }
329
+
330
+ // 请求记录
331
+ Logger.debug({
332
+ msg: '通用接口日志',
333
+ 请求路径: apiPath,
334
+ 请求方法: req.method,
335
+ 用户信息: ctx.user,
336
+ 请求体: ctx.body
337
+ });
338
+
339
+ // 登录验证 auth 有3种值 分别为 true、false、['admin', 'user']
340
+ if (api.auth === true && !ctx.user.id) {
341
+ return Response.json(RNo('未登录'));
342
+ }
343
+
344
+ if (api.auth && api.auth !== true && ctx.user.role !== api.auth) {
345
+ return Response.json(RNo('没有权限'));
346
+ }
347
+
348
+ // 参数验证
349
+ const validate = validator.validate(ctx.body, api.fields, api.required);
350
+ if (validate.code !== 0) {
351
+ return Response.json(RNo('无效的请求参数格式', validate.fields));
352
+ }
353
+
354
+ // 执行函数
355
+ const result = await api.handler(this.appContext, ctx, req);
356
+
357
+ // 返回数据
358
+ if (result && typeof result === 'object' && 'code' in result) {
359
+ return Response.json(result);
360
+ } else {
361
+ return new Response(result);
362
+ }
363
+ } catch (error) {
364
+ Logger.error({
365
+ msg: '处理接口请求时发生错误',
366
+ error: error.message,
367
+ stack: error.stack,
368
+ url: req.url
369
+ });
370
+ return Response.json(RNo('内部服务器错误'));
371
+ }
372
+ },
373
+ '/*': async (req) => {
374
+ const url = new URL(req.url);
375
+ const filePath = path.join(process.cwd(), 'public', url.pathname);
376
+
377
+ try {
378
+ const file = await Bun.file(filePath);
379
+ if (await file.exists()) {
380
+ return new Response(file, {
381
+ headers: {
382
+ 'Content-Type': file.type || 'application/octet-stream'
383
+ }
384
+ });
385
+ } else {
386
+ return Response.json(RNo('文件未找到'));
387
+ }
388
+ } catch (error) {
389
+ return Response.json(RNo('文件读取失败'));
390
+ }
391
+ },
392
+ ...(this.appOptions.routes || {})
393
+ },
394
+ error(error) {
395
+ Logger.error({
396
+ msg: '服务启动时发生错误',
397
+ error: error.message,
398
+ stack: error.stack
399
+ });
400
+ return Response.json(RNo('内部服务器错误'));
401
+ }
402
+ });
403
+
404
+ if (callback && typeof callback === 'function') {
405
+ callback(server);
406
+ }
407
+ }
408
+ }
409
+
410
+ export { BunPii, Env, Api, Jwt, Crypto2, Logger, RYes, RNo };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "befly",
3
+ "version": "0.1.26",
4
+ "description": "Buma - 为 Bun 专属打造的 API 接口框架核心引擎",
5
+ "type": "module",
6
+ "private": false,
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "registry": "https://registry.npmjs.org"
10
+ },
11
+ "main": "main.js",
12
+ "exports": {
13
+ ".": "./main.js"
14
+ },
15
+ "scripts": {
16
+ "r": "bun publish --registry=https://registry.npmjs.org --access=public"
17
+ },
18
+ "keywords": [
19
+ "bun",
20
+ "api",
21
+ "framework",
22
+ "core",
23
+ "javascript",
24
+ "backend",
25
+ "rest",
26
+ "http"
27
+ ],
28
+ "author": "chensuiyi <bimostyle@qq.com>",
29
+ "homepage": "https://chensuiyi.me",
30
+ "license": "Apache-2.0",
31
+ "files": [
32
+ "apis/",
33
+ "checks/",
34
+ "config/",
35
+ "libs/",
36
+ "plugins/",
37
+ "schema/",
38
+ "utils/",
39
+ ".gitignore",
40
+ ".npmignore",
41
+ ".bunignore",
42
+ ".editorconfig",
43
+ ".npmrc",
44
+ ".prettierignore",
45
+ ".prettierrc",
46
+ "LICENSE",
47
+ "main.js",
48
+ "package.json",
49
+ "README.md",
50
+ "vitest.config.js"
51
+ ],
52
+ "gitHead": "763f398127df3dc2471295745b8dfde3fdd84a92"
53
+ }
@@ -0,0 +1,15 @@
1
+ export default {
2
+ after: ['_redis', '_db'],
3
+ async onGet(bunpii, ctx, req) {
4
+ // 设置 CORS 头部
5
+ req.headers.set('Access-Control-Allow-Origin', req.headers.get('origin'));
6
+ req.headers.set('Access-Control-Allow-Methods', 'POST,GET,OPTIONS');
7
+ req.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
8
+ req.headers.set('Access-Control-Allow-Credentials', 'true');
9
+
10
+ // 处理预检请求
11
+ if (req.method === 'OPTIONS') {
12
+ req.status = 204;
13
+ }
14
+ }
15
+ };
package/plugins/db.js ADDED
@@ -0,0 +1,56 @@
1
+ import { Env } from '../config/env.js';
2
+ import { Logger } from '../utils/logger.js';
3
+ import { Crud } from '../utils/curd.js';
4
+
5
+ export default {
6
+ after: ['_redis'],
7
+ async onInit(bunpii) {
8
+ try {
9
+ if (Env.MYSQL_ENABLE === 1) {
10
+ // 创建 MySQL 连接池
11
+ const config = {
12
+ host: Env.MYSQL_HOST || '127.0.0.1',
13
+ port: Env.MYSQL_PORT || 3306,
14
+ database: Env.MYSQL_DB || 'test',
15
+ user: Env.MYSQL_USER || 'root',
16
+ password: Env.MYSQL_PASSWORD || 'root',
17
+ connectionLimit: Env.MYSQL_POOL_MAX || 10,
18
+ charset: 'utf8mb4_general_ci',
19
+ // timezone: Env.TIMEZONE,
20
+ debug: Env.MYSQL_DEBUG === 1
21
+ };
22
+
23
+ const createPool = await import('mysql2').then((m) => m.createPool);
24
+ const { Kysely, MysqlDialect, sql } = await import('kysely');
25
+
26
+ const pool = await createPool(config);
27
+
28
+ // 创建 Kysely 实例
29
+ const db = new Kysely({
30
+ dialect: new MysqlDialect({
31
+ pool: pool
32
+ })
33
+ });
34
+
35
+ // 测试数据库连接
36
+ const result = await sql`SELECT VERSION() AS version`.execute(db);
37
+ if (result?.rows?.[0]?.version) {
38
+ // 扩展数据库实例
39
+ return new Crud(db, bunpii.redis, sql);
40
+ } else {
41
+ return {};
42
+ }
43
+ } else {
44
+ Logger.warn(`Mysql 未启用,跳过初始化`);
45
+ return {};
46
+ }
47
+ } catch (error) {
48
+ Logger.error({
49
+ msg: '数据库连接失败',
50
+ message: error.message,
51
+ stack: error.stack
52
+ });
53
+ process.exit();
54
+ }
55
+ }
56
+ };
@@ -0,0 +1,13 @@
1
+ import { Env } from '../config/env.js';
2
+ import { Logger } from '../utils/logger.js';
3
+
4
+ export default {
5
+ after: [],
6
+ async onInit(bunpii) {
7
+ try {
8
+ return Logger;
9
+ } catch (error) {
10
+ process.exit();
11
+ }
12
+ }
13
+ };
@@ -0,0 +1,116 @@
1
+ import { Env } from '../config/env.js';
2
+ import { Logger } from '../utils/logger.js';
3
+
4
+ export default {
5
+ after: ['_logger'],
6
+ async onInit(bunpii) {
7
+ try {
8
+ if (Env.REDIS_ENABLE === 1) {
9
+ const config = {
10
+ username: Env.REDIS_USERNAME || '',
11
+ password: Env.REDIS_PASSWORD || '',
12
+ database: Env.REDIS_DB || 0,
13
+ socket: {
14
+ host: Env.REDIS_HOST || '127.0.0.1',
15
+ port: Env.REDIS_PORT || 6379,
16
+ reconnectStrategy: (retries) => {
17
+ // 指数退避重连策略,最大延迟 2 秒
18
+ const jitter = Math.floor(Math.random() * 200);
19
+ const delay = Math.min(Math.pow(2, retries) * 50, 2000);
20
+ return delay + jitter;
21
+ }
22
+ }
23
+ };
24
+ const createClient = await import('@redis/client').then((m) => m.createClient);
25
+ const redis = createClient(config);
26
+
27
+ // 测试连接
28
+ try {
29
+ await redis.connect();
30
+ // 测试连接
31
+ const result = await redis.ping();
32
+ } catch (error) {
33
+ Logger.error({
34
+ msg: 'Redis 连接失败',
35
+ message: error.message,
36
+ stack: error.stack
37
+ });
38
+ process.exit();
39
+ }
40
+
41
+ // 添加对象存储辅助方法
42
+ redis.setObject = async (key, obj, ttl = null) => {
43
+ try {
44
+ const data = JSON.stringify(obj);
45
+ if (ttl) {
46
+ return await redis.setEx(`${process.env.REDIS_KEY_PREFIX}:${key}`, ttl, data);
47
+ }
48
+ return await redis.set(`${process.env.REDIS_KEY_PREFIX}:${key}`, data);
49
+ } catch (error) {
50
+ Logger.error({
51
+ msg: 'Redis setObject 错误',
52
+ message: error.message,
53
+ stack: error.stack
54
+ });
55
+ }
56
+ };
57
+
58
+ redis.getObject = async (key) => {
59
+ try {
60
+ const data = await redis.get(`${process.env.REDIS_KEY_PREFIX}:${key}`);
61
+ return data ? JSON.parse(data) : null;
62
+ } catch (error) {
63
+ Logger.error({
64
+ msg: 'Redis getObject 错误',
65
+ message: error.message,
66
+ stack: error.stack
67
+ });
68
+ return null;
69
+ }
70
+ };
71
+
72
+ redis.delObject = async (key) => {
73
+ try {
74
+ await redis.del(`${process.env.REDIS_KEY_PREFIX}:${key}`);
75
+ } catch (error) {
76
+ Logger.error({
77
+ msg: 'Redis delObject 错误',
78
+ message: error.message,
79
+ stack: error.stack
80
+ });
81
+ }
82
+ };
83
+
84
+ // 添加时序ID生成函数
85
+ redis.genTimeID = async () => {
86
+ const timestamp = Math.floor(Date.now() / 1000);
87
+ const key = `time_id_counter:${timestamp}`;
88
+
89
+ const counter = await redis.incr(key);
90
+ await redis.expire(key, 2);
91
+
92
+ // 前3位计数器 + 后3位随机数
93
+ const counterPrefix = (counter % 1000).toString().padStart(3, '0'); // 000-999
94
+ const randomSuffix = Math.floor(Math.random() * 1000)
95
+ .toString()
96
+ .padStart(3, '0'); // 000-999
97
+ const suffix = `${counterPrefix}${randomSuffix}`;
98
+
99
+ return Number(`${timestamp}${suffix}`);
100
+ };
101
+
102
+ return redis;
103
+ } else {
104
+ Logger.warn(`Redis 未启用,跳过初始化`);
105
+ return {};
106
+ }
107
+ } catch (err) {
108
+ Logger.error({
109
+ msg: 'Redis 初始化失败',
110
+ message: err.message,
111
+ stack: err.stack
112
+ });
113
+ process.exit();
114
+ }
115
+ }
116
+ };
@@ -0,0 +1,17 @@
1
+ {
2
+ "id": "ID,number,1,999999999,x%2=0",
3
+ "email": "邮箱,string,5,100,^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
4
+ "phone": "手机号,string,11,11,^1[3-9]\\d{9}$",
5
+ "page": "页码,number,1,9999,null",
6
+ "limit": "每页数量,number,1,100,null",
7
+ "title": "标题,string,1,200,null",
8
+ "description": "描述,string,0,500,null",
9
+ "keyword": "关键词,string,1,50,null",
10
+ "status": "状态,string,1,20,null",
11
+ "enabled": "启用状态,number,0,1,null",
12
+ "date": "日期,string,10,10,^\\d{4}-\\d{2}-\\d{2}$",
13
+ "datetime": "日期时间,string,19,25,^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}",
14
+ "filename": "文件名,string,1,255,null",
15
+ "url": "网址,string,5,500,^https?://",
16
+ "tag": "标签,array,0,10,null"
17
+ }
@@ -0,0 +1 @@
1
+ {}