koa3-cli 1.0.4 → 1.0.6
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 +31 -1
- package/app/controller/user.js +35 -24
- package/app/lib/logger.js +141 -0
- package/app/middleware/errorHandler.js +28 -0
- package/app/middleware/notFound.js +11 -0
- package/app/middleware/requestLogger.js +27 -0
- package/app/processEvents.js +14 -0
- package/app/setup.js +48 -0
- package/app.js +22 -95
- package/config/config.default.js +26 -25
- package/config/loader.js +25 -0
- package/env.example +4 -0
- package/package.json +6 -6
- package/public/index.html +37 -5
package/README.md
CHANGED
|
@@ -49,12 +49,16 @@ koa3-cli/
|
|
|
49
49
|
│ │ └── user.js # 用户模型
|
|
50
50
|
│ ├── middleware/ # 中间件目录
|
|
51
51
|
│ │ ├── index.js # 中间件入口
|
|
52
|
-
│ │
|
|
52
|
+
│ │ ├── auth.js # 认证中间件示例
|
|
53
|
+
│ │ └── requestLogger.js # 请求日志中间件
|
|
54
|
+
│ ├── lib/ # 基础能力目录
|
|
55
|
+
│ │ └── logger.js # 日志工具
|
|
53
56
|
│ └── router.js # 路由配置
|
|
54
57
|
├── config/ # 配置文件目录
|
|
55
58
|
│ ├── config.default.js # 默认配置
|
|
56
59
|
│ ├── config.local.js # 本地开发配置
|
|
57
60
|
│ └── config.prod.js # 生产环境配置
|
|
61
|
+
├── logs/ # 日志输出目录(运行时自动创建)
|
|
58
62
|
├── public/ # 静态资源目录
|
|
59
63
|
│ └── index.html # 首页
|
|
60
64
|
├── app.js # 应用入口文件
|
|
@@ -71,6 +75,7 @@ koa3-cli/
|
|
|
71
75
|
- ✅ MVC 架构(Controller/Service/Model)
|
|
72
76
|
- ✅ 中间件支持
|
|
73
77
|
- ✅ 统一的错误处理
|
|
78
|
+
- ✅ 内置日志系统(访问日志、错误日志、请求追踪)
|
|
74
79
|
- ✅ RESTful API 示例
|
|
75
80
|
|
|
76
81
|
## 快速开始
|
|
@@ -109,6 +114,31 @@ npm start
|
|
|
109
114
|
|
|
110
115
|
可以通过 `.env` 文件配置环境变量(参考 `.env.example`)。
|
|
111
116
|
|
|
117
|
+
### 日志配置
|
|
118
|
+
|
|
119
|
+
日志系统默认开启控制台和文件输出,支持 4 个级别:`debug`、`info`、`warn`、`error`。
|
|
120
|
+
|
|
121
|
+
`.env` 可配置项:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
LOG_LEVEL=info
|
|
125
|
+
LOG_DIR=logs
|
|
126
|
+
LOG_ENABLE_CONSOLE=true
|
|
127
|
+
LOG_ENABLE_FILE=true
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
运行后会在日志目录按天生成文件:
|
|
131
|
+
|
|
132
|
+
- `<date>.log`:通用应用日志
|
|
133
|
+
- `<date>.access.log`:请求访问日志(JSON 行格式)
|
|
134
|
+
- `<date>.error.log`:错误级别日志
|
|
135
|
+
|
|
136
|
+
请求日志中间件会自动处理 `x-request-id`:
|
|
137
|
+
|
|
138
|
+
- 如果请求头带有 `x-request-id`,服务端会透传并写入日志
|
|
139
|
+
- 如果没有,服务端会自动生成并在响应头返回
|
|
140
|
+
- 出错日志会带上 `requestId`,便于串联排查
|
|
141
|
+
|
|
112
142
|
## API 示例
|
|
113
143
|
|
|
114
144
|
### 获取用户列表
|
package/app/controller/user.js
CHANGED
|
@@ -1,86 +1,97 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
const userService = require('../service/user');
|
|
2
|
+
|
|
3
|
+
function logMeta(ctx, extra = {}) {
|
|
4
|
+
return {
|
|
5
|
+
requestId: ctx.state && ctx.state.requestId,
|
|
6
|
+
method: ctx.method,
|
|
7
|
+
url: ctx.originalUrl || ctx.url,
|
|
8
|
+
...extra
|
|
9
|
+
};
|
|
10
|
+
}
|
|
5
11
|
|
|
6
12
|
class UserController {
|
|
7
|
-
/**
|
|
8
|
-
* 获取用户列表
|
|
9
|
-
*/
|
|
10
13
|
async list(ctx) {
|
|
11
14
|
try {
|
|
12
15
|
const users = await userService.getUserList();
|
|
16
|
+
ctx.logger.info('User list fetched', logMeta(ctx, { count: Array.isArray(users) ? users.length : undefined }));
|
|
13
17
|
ctx.body = users;
|
|
14
18
|
} catch (error) {
|
|
19
|
+
ctx.logger.error('Failed to fetch user list', logMeta(ctx, { message: error.message, stack: error.stack }));
|
|
15
20
|
ctx.throw(500, error.message);
|
|
16
21
|
}
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
/**
|
|
20
|
-
* 获取用户详情
|
|
21
|
-
*/
|
|
22
24
|
async detail(ctx) {
|
|
25
|
+
const { id } = ctx.params;
|
|
26
|
+
|
|
23
27
|
try {
|
|
24
|
-
const { id } = ctx.params;
|
|
25
28
|
const user = await userService.getUserById(id);
|
|
26
29
|
if (!user) {
|
|
30
|
+
ctx.logger.warn('User detail not found', logMeta(ctx, { userId: id }));
|
|
27
31
|
ctx.status = 404;
|
|
28
32
|
ctx.body = { message: 'User not found' };
|
|
29
33
|
return;
|
|
30
34
|
}
|
|
35
|
+
|
|
36
|
+
ctx.logger.info('User detail fetched', logMeta(ctx, { userId: id }));
|
|
31
37
|
ctx.body = user;
|
|
32
38
|
} catch (error) {
|
|
39
|
+
ctx.logger.error('Failed to fetch user detail', logMeta(ctx, { userId: id, message: error.message, stack: error.stack }));
|
|
33
40
|
ctx.throw(500, error.message);
|
|
34
41
|
}
|
|
35
42
|
}
|
|
36
43
|
|
|
37
|
-
/**
|
|
38
|
-
* 创建用户
|
|
39
|
-
*/
|
|
40
44
|
async create(ctx) {
|
|
45
|
+
const userData = ctx.request.body;
|
|
46
|
+
|
|
41
47
|
try {
|
|
42
|
-
const userData = ctx.request.body;
|
|
43
48
|
const user = await userService.createUser(userData);
|
|
49
|
+
ctx.logger.info('User created', logMeta(ctx, { userId: user && user.id }));
|
|
44
50
|
ctx.status = 201;
|
|
45
51
|
ctx.body = user;
|
|
46
52
|
} catch (error) {
|
|
53
|
+
ctx.logger.error('Failed to create user', logMeta(ctx, { message: error.message, stack: error.stack }));
|
|
47
54
|
ctx.throw(500, error.message);
|
|
48
55
|
}
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
/**
|
|
52
|
-
* 更新用户
|
|
53
|
-
*/
|
|
54
58
|
async update(ctx) {
|
|
59
|
+
const { id } = ctx.params;
|
|
60
|
+
const userData = ctx.request.body;
|
|
61
|
+
|
|
55
62
|
try {
|
|
56
|
-
const { id } = ctx.params;
|
|
57
|
-
const userData = ctx.request.body;
|
|
58
63
|
const user = await userService.updateUser(id, userData);
|
|
59
64
|
if (!user) {
|
|
65
|
+
ctx.logger.warn('User update target not found', logMeta(ctx, { userId: id }));
|
|
60
66
|
ctx.status = 404;
|
|
61
67
|
ctx.body = { message: 'User not found' };
|
|
62
68
|
return;
|
|
63
69
|
}
|
|
70
|
+
|
|
71
|
+
ctx.logger.info('User updated', logMeta(ctx, { userId: id }));
|
|
64
72
|
ctx.body = user;
|
|
65
73
|
} catch (error) {
|
|
74
|
+
ctx.logger.error('Failed to update user', logMeta(ctx, { userId: id, message: error.message, stack: error.stack }));
|
|
66
75
|
ctx.throw(500, error.message);
|
|
67
76
|
}
|
|
68
77
|
}
|
|
69
78
|
|
|
70
|
-
/**
|
|
71
|
-
* 删除用户
|
|
72
|
-
*/
|
|
73
79
|
async delete(ctx) {
|
|
80
|
+
const { id } = ctx.params;
|
|
81
|
+
|
|
74
82
|
try {
|
|
75
|
-
const { id } = ctx.params;
|
|
76
83
|
const result = await userService.deleteUser(id);
|
|
77
84
|
if (!result) {
|
|
85
|
+
ctx.logger.warn('User delete target not found', logMeta(ctx, { userId: id }));
|
|
78
86
|
ctx.status = 404;
|
|
79
87
|
ctx.body = { message: 'User not found' };
|
|
80
88
|
return;
|
|
81
89
|
}
|
|
90
|
+
|
|
91
|
+
ctx.logger.info('User deleted', logMeta(ctx, { userId: id }));
|
|
82
92
|
ctx.status = 204;
|
|
83
93
|
} catch (error) {
|
|
94
|
+
ctx.logger.error('Failed to delete user', logMeta(ctx, { userId: id, message: error.message, stack: error.stack }));
|
|
84
95
|
ctx.throw(500, error.message);
|
|
85
96
|
}
|
|
86
97
|
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const util = require('util');
|
|
4
|
+
|
|
5
|
+
const LEVEL_WEIGHT = {
|
|
6
|
+
debug: 10,
|
|
7
|
+
info: 20,
|
|
8
|
+
warn: 30,
|
|
9
|
+
error: 40
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function normalizeLevel(level) {
|
|
13
|
+
const resolved = String(level || 'info').toLowerCase();
|
|
14
|
+
return LEVEL_WEIGHT[resolved] ? resolved : 'info';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatDate(date = new Date()) {
|
|
18
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
19
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function formatTimestamp(date = new Date()) {
|
|
23
|
+
return date.toLocaleString();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function safeSerialize(meta) {
|
|
27
|
+
if (meta === undefined) {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (meta instanceof Error) {
|
|
32
|
+
return JSON.stringify({
|
|
33
|
+
name: meta.name,
|
|
34
|
+
message: meta.message,
|
|
35
|
+
stack: meta.stack
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof meta === 'string') {
|
|
40
|
+
return meta;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
return JSON.stringify(meta);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return util.inspect(meta, { depth: 4, breakLength: 120 });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class Logger {
|
|
51
|
+
constructor(options = {}) {
|
|
52
|
+
this.level = normalizeLevel(options.level);
|
|
53
|
+
this.enableConsole = options.enableConsole !== false;
|
|
54
|
+
this.enableFile = options.enableFile !== false;
|
|
55
|
+
this.dir = options.dir || 'logs';
|
|
56
|
+
this.appName = options.appName || 'koa3-cli';
|
|
57
|
+
this.cwd = options.cwd || process.cwd();
|
|
58
|
+
this.logDir = path.isAbsolute(this.dir) ? this.dir : path.join(this.cwd, this.dir);
|
|
59
|
+
|
|
60
|
+
if (this.enableFile) {
|
|
61
|
+
fs.mkdirSync(this.logDir, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
shouldLog(level) {
|
|
66
|
+
return LEVEL_WEIGHT[level] >= LEVEL_WEIGHT[this.level];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
write(level, message, meta) {
|
|
70
|
+
if (!this.shouldLog(level)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const timestamp = formatTimestamp();
|
|
75
|
+
const metaText = safeSerialize(meta);
|
|
76
|
+
const line = `[${timestamp}] [${this.appName}] [${level.toUpperCase()}] ${message}${metaText ? ` ${metaText}` : ''}`;
|
|
77
|
+
|
|
78
|
+
if (this.enableConsole) {
|
|
79
|
+
const printer = level === 'error' ? console.error : level === 'warn' ? console.warn : console.log;
|
|
80
|
+
printer(line);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!this.enableFile) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const date = formatDate();
|
|
88
|
+
const commonPath = path.join(this.logDir, `${date}.log`);
|
|
89
|
+
fs.appendFile(commonPath, `${line}\n`, () => {});
|
|
90
|
+
|
|
91
|
+
if (level === 'error') {
|
|
92
|
+
const errorPath = path.join(this.logDir, `${date}.error.log`);
|
|
93
|
+
fs.appendFile(errorPath, `${line}\n`, () => {});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
access(data) {
|
|
98
|
+
const line = {
|
|
99
|
+
timestamp: formatTimestamp(),
|
|
100
|
+
type: 'access',
|
|
101
|
+
app: this.appName,
|
|
102
|
+
...data
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
if (this.enableConsole && this.shouldLog('info')) {
|
|
106
|
+
console.log(`[${line.timestamp}] [${this.appName}] [ACCESS] ${line.method} ${line.url} ${line.status} ${line.duration}ms ${line.requestId}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!this.enableFile) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const date = formatDate();
|
|
114
|
+
const accessPath = path.join(this.logDir, `${date}.access.log`);
|
|
115
|
+
fs.appendFile(accessPath, `${JSON.stringify(line)}\n`, () => {});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
debug(message, meta) {
|
|
119
|
+
this.write('debug', message, meta);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
info(message, meta) {
|
|
123
|
+
this.write('info', message, meta);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
warn(message, meta) {
|
|
127
|
+
this.write('warn', message, meta);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
error(message, meta) {
|
|
131
|
+
this.write('error', message, meta);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function createLogger(options) {
|
|
136
|
+
return new Logger(options);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
createLogger
|
|
141
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 全局错误处理中间件:捕获异常、记录日志、统一错误响应
|
|
3
|
+
*/
|
|
4
|
+
module.exports = function createErrorHandler(config, logger) {
|
|
5
|
+
return async function errorHandler(ctx, next) {
|
|
6
|
+
try {
|
|
7
|
+
await next();
|
|
8
|
+
} catch (err) {
|
|
9
|
+
ctx.status = err.status || 500;
|
|
10
|
+
ctx.body = {
|
|
11
|
+
success: false,
|
|
12
|
+
message: err.message || 'Internal Server Error',
|
|
13
|
+
...(config.env === 'development' && { stack: err.stack })
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
logger.error('Request failed', {
|
|
17
|
+
requestId: ctx.state && ctx.state.requestId,
|
|
18
|
+
method: ctx.method,
|
|
19
|
+
url: ctx.originalUrl || ctx.url,
|
|
20
|
+
status: ctx.status,
|
|
21
|
+
message: err.message,
|
|
22
|
+
stack: err.stack
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
ctx.app.emit('error', err, ctx);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
function createRequestId() {
|
|
2
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
module.exports = function requestLogger(logger) {
|
|
6
|
+
return async function requestLoggerMiddleware(ctx, next) {
|
|
7
|
+
const start = Date.now();
|
|
8
|
+
const requestId = ctx.get('x-request-id') || createRequestId();
|
|
9
|
+
|
|
10
|
+
ctx.state.requestId = requestId;
|
|
11
|
+
ctx.set('x-request-id', requestId);
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
await next();
|
|
15
|
+
} finally {
|
|
16
|
+
const duration = Date.now() - start;
|
|
17
|
+
logger.access({
|
|
18
|
+
requestId,
|
|
19
|
+
method: ctx.method,
|
|
20
|
+
url: ctx.originalUrl || ctx.url,
|
|
21
|
+
status: ctx.status,
|
|
22
|
+
duration,
|
|
23
|
+
ip: ctx.ip
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 进程级错误监听:未捕获的 Promise 与异常
|
|
3
|
+
*/
|
|
4
|
+
function setupProcessEvents(logger) {
|
|
5
|
+
process.on('unhandledRejection', (reason) => {
|
|
6
|
+
logger.error('Unhandled promise rejection', reason);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
process.on('uncaughtException', (error) => {
|
|
10
|
+
logger.error('Uncaught exception', error);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = setupProcessEvents;
|
package/app/setup.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 应用挂载:静态资源、视图、中间件、路由等
|
|
3
|
+
* 保持 app.js 只做「创建实例 + 调用 setup + 监听 + 启动」
|
|
4
|
+
*/
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const bodyParser = require('koa-bodyparser');
|
|
8
|
+
const serveStatic = require('koa-static');
|
|
9
|
+
const views = require('@ladjs/koa-views');
|
|
10
|
+
|
|
11
|
+
const createRequestLogger = require('./middleware/requestLogger');
|
|
12
|
+
const createErrorHandler = require('./middleware/errorHandler');
|
|
13
|
+
const notFound = require('./middleware/notFound');
|
|
14
|
+
const middleware = require('./middleware');
|
|
15
|
+
const router = require('./router');
|
|
16
|
+
|
|
17
|
+
const rootDir = path.join(__dirname, '..');
|
|
18
|
+
|
|
19
|
+
function setup(app, config, logger) {
|
|
20
|
+
// 静态资源
|
|
21
|
+
if (config.static && config.static.enable !== false) {
|
|
22
|
+
const staticPath = path.join(rootDir, config.static.dir || 'public');
|
|
23
|
+
if (fs.existsSync(staticPath)) {
|
|
24
|
+
app.use(serveStatic(staticPath, config.static.options || {}));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 视图
|
|
29
|
+
if (config.view && config.view.enable !== false) {
|
|
30
|
+
const viewPath = path.join(rootDir, config.view.root || 'app/view');
|
|
31
|
+
if (fs.existsSync(viewPath)) {
|
|
32
|
+
app.use(views(viewPath, config.view.options || { extension: 'ejs' }));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
app.use(bodyParser(config.bodyParser || {}));
|
|
37
|
+
app.use(createRequestLogger(logger));
|
|
38
|
+
|
|
39
|
+
if (middleware && typeof middleware === 'function') {
|
|
40
|
+
app.use(middleware);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
app.use(createErrorHandler(config, logger));
|
|
44
|
+
app.use(router.routes()).use(router.allowedMethods());
|
|
45
|
+
app.use(notFound);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = setup;
|
package/app.js
CHANGED
|
@@ -1,111 +1,38 @@
|
|
|
1
|
-
const Koa = require('koa');
|
|
2
|
-
const bodyParser = require('koa-bodyparser');
|
|
3
|
-
const static = require('koa-static');
|
|
4
|
-
const views = require('@ladjs/koa-views');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
|
|
8
|
-
// 加载环境变量
|
|
9
1
|
require('dotenv').config();
|
|
10
2
|
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (env === 'production') {
|
|
17
|
-
envConfig = require('./config/config.prod');
|
|
18
|
-
} else if (env === 'local' || env === 'development') {
|
|
19
|
-
envConfig = require('./config/config.local');
|
|
20
|
-
}
|
|
21
|
-
} catch (e) {
|
|
22
|
-
// 环境配置文件不存在时忽略
|
|
23
|
-
}
|
|
24
|
-
const config = Object.assign({}, defaultConfig, envConfig);
|
|
25
|
-
|
|
26
|
-
// 加载中间件
|
|
27
|
-
const middleware = require('./app/middleware');
|
|
28
|
-
|
|
29
|
-
// 加载路由
|
|
30
|
-
const router = require('./app/router');
|
|
3
|
+
const Koa = require('koa');
|
|
4
|
+
const { loadConfig } = require('./config/loader');
|
|
5
|
+
const { createLogger } = require('./app/lib/logger');
|
|
6
|
+
const setup = require('./app/setup');
|
|
7
|
+
const setupProcessEvents = require('./app/processEvents');
|
|
31
8
|
|
|
9
|
+
const config = loadConfig();
|
|
32
10
|
const app = new Koa();
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// 静态资源
|
|
38
|
-
if (config.static && config.static.enable !== false) {
|
|
39
|
-
const staticPath = path.join(__dirname, config.static.dir || 'public');
|
|
40
|
-
if (fs.existsSync(staticPath)) {
|
|
41
|
-
app.use(static(staticPath, config.static.options || {}));
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// 模板引擎
|
|
46
|
-
if (config.view && config.view.enable !== false) {
|
|
47
|
-
const viewPath = path.join(__dirname, config.view.root || 'app/view');
|
|
48
|
-
if (fs.existsSync(viewPath)) {
|
|
49
|
-
app.use(views(viewPath, config.view.options || {
|
|
50
|
-
extension: 'ejs'
|
|
51
|
-
}));
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// 请求体解析
|
|
56
|
-
app.use(bodyParser(config.bodyParser || {}));
|
|
57
|
-
|
|
58
|
-
// 自定义中间件
|
|
59
|
-
if (middleware && typeof middleware === 'function') {
|
|
60
|
-
app.use(middleware);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// 错误处理中间件
|
|
64
|
-
app.use(async (ctx, next) => {
|
|
65
|
-
try {
|
|
66
|
-
await next();
|
|
67
|
-
} catch (err) {
|
|
68
|
-
ctx.status = err.status || 500;
|
|
69
|
-
ctx.body = {
|
|
70
|
-
success: false,
|
|
71
|
-
message: err.message || 'Internal Server Error',
|
|
72
|
-
...(config.env === 'development' && { stack: err.stack })
|
|
73
|
-
};
|
|
74
|
-
ctx.app.emit('error', err, ctx);
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// 日志中间件
|
|
79
|
-
app.use(async (ctx, next) => {
|
|
80
|
-
const start = Date.now();
|
|
81
|
-
await next();
|
|
82
|
-
const ms = Date.now() - start;
|
|
83
|
-
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
|
|
11
|
+
const logger = createLogger({
|
|
12
|
+
...(config.logger || {}),
|
|
13
|
+
appName: config.name || 'koa3-cli',
|
|
14
|
+
cwd: __dirname
|
|
84
15
|
});
|
|
85
16
|
|
|
86
|
-
|
|
87
|
-
app.
|
|
17
|
+
app.keys = config.keys || ['koa3-cli-secret-key'];
|
|
18
|
+
app.context.logger = logger;
|
|
88
19
|
|
|
89
|
-
|
|
90
|
-
app.use(async (ctx) => {
|
|
91
|
-
if (ctx.status === 404) {
|
|
92
|
-
ctx.body = {
|
|
93
|
-
success: false,
|
|
94
|
-
message: 'Not Found'
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
});
|
|
20
|
+
setup(app, config, logger);
|
|
98
21
|
|
|
99
|
-
// 错误事件监听
|
|
100
22
|
app.on('error', (err, ctx) => {
|
|
101
|
-
|
|
23
|
+
logger.error('Server error event', {
|
|
24
|
+
requestId: ctx && ctx.state && ctx.state.requestId,
|
|
25
|
+
message: err.message,
|
|
26
|
+
stack: err.stack
|
|
27
|
+
});
|
|
102
28
|
});
|
|
103
29
|
|
|
104
|
-
|
|
30
|
+
setupProcessEvents(logger);
|
|
31
|
+
|
|
105
32
|
const port = config.port || 3000;
|
|
106
33
|
app.listen(port, () => {
|
|
107
|
-
|
|
108
|
-
|
|
34
|
+
logger.info(`Server is running on http://localhost:${port}`);
|
|
35
|
+
logger.info(`Environment: ${config.env}`);
|
|
109
36
|
});
|
|
110
37
|
|
|
111
38
|
module.exports = app;
|
package/config/config.default.js
CHANGED
|
@@ -1,37 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* 所有环境都会加载此配置
|
|
2
|
+
* Default config loaded in all environments.
|
|
4
3
|
*/
|
|
5
4
|
module.exports = {
|
|
6
|
-
//
|
|
5
|
+
// Application name
|
|
7
6
|
name: 'koa3-cli',
|
|
8
|
-
|
|
9
|
-
//
|
|
7
|
+
|
|
8
|
+
// Runtime env: development, production, test
|
|
10
9
|
env: process.env.NODE_ENV || 'development',
|
|
11
|
-
|
|
12
|
-
//
|
|
10
|
+
|
|
11
|
+
// Server port
|
|
13
12
|
port: process.env.PORT || 3000,
|
|
14
|
-
|
|
15
|
-
//
|
|
13
|
+
|
|
14
|
+
// Cookie signing keys
|
|
16
15
|
keys: process.env.KEYS ? process.env.KEYS.split(',') : ['koa3-cli-secret-key'],
|
|
17
|
-
|
|
18
|
-
//
|
|
16
|
+
|
|
17
|
+
// Static assets
|
|
19
18
|
static: {
|
|
20
19
|
enable: true,
|
|
21
20
|
dir: 'public',
|
|
22
21
|
options: {
|
|
23
|
-
maxAge: 365 * 24 * 60 * 60 * 1000,
|
|
22
|
+
maxAge: 365 * 24 * 60 * 60 * 1000,
|
|
24
23
|
gzip: true
|
|
25
24
|
}
|
|
26
25
|
},
|
|
27
|
-
|
|
28
|
-
//
|
|
26
|
+
|
|
27
|
+
// Docs build config
|
|
29
28
|
docs: {
|
|
30
29
|
enable: true,
|
|
31
30
|
buildDir: 'public/docs'
|
|
32
31
|
},
|
|
33
|
-
|
|
34
|
-
//
|
|
32
|
+
|
|
33
|
+
// View engine
|
|
35
34
|
view: {
|
|
36
35
|
enable: true,
|
|
37
36
|
root: 'app/view',
|
|
@@ -42,15 +41,15 @@ module.exports = {
|
|
|
42
41
|
}
|
|
43
42
|
}
|
|
44
43
|
},
|
|
45
|
-
|
|
46
|
-
// bodyParser
|
|
44
|
+
|
|
45
|
+
// bodyParser
|
|
47
46
|
bodyParser: {
|
|
48
47
|
enableTypes: ['json', 'form', 'text'],
|
|
49
48
|
jsonLimit: '10mb',
|
|
50
49
|
formLimit: '10mb'
|
|
51
50
|
},
|
|
52
|
-
|
|
53
|
-
//
|
|
51
|
+
|
|
52
|
+
// Database (example)
|
|
54
53
|
database: {
|
|
55
54
|
client: 'mysql',
|
|
56
55
|
connection: {
|
|
@@ -65,18 +64,20 @@ module.exports = {
|
|
|
65
64
|
max: 10
|
|
66
65
|
}
|
|
67
66
|
},
|
|
68
|
-
|
|
69
|
-
// Redis
|
|
67
|
+
|
|
68
|
+
// Redis (example)
|
|
70
69
|
redis: {
|
|
71
70
|
host: process.env.REDIS_HOST || 'localhost',
|
|
72
71
|
port: process.env.REDIS_PORT || 6379,
|
|
73
72
|
password: process.env.REDIS_PASSWORD || '',
|
|
74
73
|
db: process.env.REDIS_DB || 0
|
|
75
74
|
},
|
|
76
|
-
|
|
77
|
-
//
|
|
75
|
+
|
|
76
|
+
// Logger
|
|
78
77
|
logger: {
|
|
79
78
|
level: process.env.LOG_LEVEL || 'info',
|
|
80
|
-
dir: 'logs'
|
|
79
|
+
dir: process.env.LOG_DIR || 'logs',
|
|
80
|
+
enableConsole: process.env.LOG_ENABLE_CONSOLE !== 'false',
|
|
81
|
+
enableFile: process.env.LOG_ENABLE_FILE !== 'false'
|
|
81
82
|
}
|
|
82
83
|
};
|
package/config/loader.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置加载器:合并 default + 环境配置
|
|
3
|
+
*/
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const defaultConfig = require('./config.default');
|
|
7
|
+
|
|
8
|
+
function loadConfig() {
|
|
9
|
+
const env = process.env.NODE_ENV || 'development';
|
|
10
|
+
let envConfig = {};
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
if (env === 'production') {
|
|
14
|
+
envConfig = require('./config.prod');
|
|
15
|
+
} else if (env === 'local' || env === 'development') {
|
|
16
|
+
envConfig = require('./config.local');
|
|
17
|
+
}
|
|
18
|
+
} catch (e) {
|
|
19
|
+
// 忽略缺失的环境配置文件
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return Object.assign({}, defaultConfig, envConfig);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = { loadConfig };
|
package/env.example
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koa3-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Koa3脚手架",
|
|
5
5
|
"main": "app.js",
|
|
6
6
|
"bin": {
|
|
@@ -25,17 +25,17 @@
|
|
|
25
25
|
"homepage": "https://atwzc.cn/",
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@koa/router": "^15.
|
|
28
|
+
"@koa/router": "^15.3.1",
|
|
29
29
|
"@ladjs/koa-views": "^9.0.0",
|
|
30
|
-
"dotenv": "^17.
|
|
31
|
-
"ejs": "^
|
|
32
|
-
"koa": "^3.1.
|
|
30
|
+
"dotenv": "^17.3.1",
|
|
31
|
+
"ejs": "^4.0.1",
|
|
32
|
+
"koa": "^3.1.2",
|
|
33
33
|
"koa-bodyparser": "^4.4.1",
|
|
34
34
|
"koa-cors": "^0.0.16",
|
|
35
35
|
"koa-static": "^5.0.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"nodemon": "^3.1.
|
|
38
|
+
"nodemon": "^3.1.14"
|
|
39
39
|
},
|
|
40
40
|
"volta": {
|
|
41
41
|
"node": "20.18.1"
|
package/public/index.html
CHANGED
|
@@ -69,6 +69,10 @@
|
|
|
69
69
|
<h3>✅ 错误处理</h3>
|
|
70
70
|
<p>统一的错误处理机制</p>
|
|
71
71
|
</div>
|
|
72
|
+
<div class="feature">
|
|
73
|
+
<h3>📝 日志系统</h3>
|
|
74
|
+
<p>支持访问日志、错误日志和 requestId 链路追踪</p>
|
|
75
|
+
</div>
|
|
72
76
|
<div class="feature">
|
|
73
77
|
<h3>⚡ CLI 工具</h3>
|
|
74
78
|
<p>一键创建项目,快速上手</p>
|
|
@@ -144,7 +148,10 @@ npm run dev</code></pre>
|
|
|
144
148
|
│ │ └── user.js # 用户模型
|
|
145
149
|
│ ├── middleware/ # 中间件目录
|
|
146
150
|
│ │ ├── index.js # 中间件入口
|
|
147
|
-
│ │
|
|
151
|
+
│ │ ├── auth.js # 认证中间件示例
|
|
152
|
+
│ │ └── requestLogger.js # 请求日志中间件
|
|
153
|
+
│ ├── lib/ # 基础能力目录
|
|
154
|
+
│ │ └── logger.js # 日志工具
|
|
148
155
|
│ ├── router.js # 路由配置
|
|
149
156
|
│ └── view/ # 视图模板目录(可选)
|
|
150
157
|
│ └── docs.ejs # 页面模板
|
|
@@ -157,6 +164,7 @@ npm run dev</code></pre>
|
|
|
157
164
|
├── public/ # 静态资源目录
|
|
158
165
|
│ ├── docs/ # 文档资源
|
|
159
166
|
│ └── index.html # 文档首页
|
|
167
|
+
├── logs/ # 日志目录(运行时自动创建)
|
|
160
168
|
├── app.js # 应用入口文件
|
|
161
169
|
├── package.json # 项目配置
|
|
162
170
|
├── env.example # 环境变量示例
|
|
@@ -237,7 +245,16 @@ REDIS_PASSWORD=
|
|
|
237
245
|
REDIS_DB=0
|
|
238
246
|
|
|
239
247
|
# 日志级别
|
|
240
|
-
LOG_LEVEL=info
|
|
248
|
+
LOG_LEVEL=info
|
|
249
|
+
|
|
250
|
+
# 日志目录
|
|
251
|
+
LOG_DIR=logs
|
|
252
|
+
|
|
253
|
+
# 是否输出到控制台
|
|
254
|
+
LOG_ENABLE_CONSOLE=true
|
|
255
|
+
|
|
256
|
+
# 是否输出到文件
|
|
257
|
+
LOG_ENABLE_FILE=true</code></pre>
|
|
241
258
|
<h2>配置项说明</h2>
|
|
242
259
|
<h3>基础配置</h3>
|
|
243
260
|
<pre><code class="language-javascript">{
|
|
@@ -288,11 +305,26 @@ LOG_LEVEL=info</code></pre>
|
|
|
288
305
|
password: '',
|
|
289
306
|
db: 0
|
|
290
307
|
}</code></pre>
|
|
291
|
-
|
|
292
|
-
|
|
308
|
+
<h3>日志配置</h3>
|
|
309
|
+
<pre><code class="language-javascript">logger: {
|
|
293
310
|
level: 'info', // 日志级别: debug, info, warn, error
|
|
294
|
-
dir: 'logs'
|
|
311
|
+
dir: 'logs', // 日志目录
|
|
312
|
+
enableConsole: true, // 是否输出到控制台
|
|
313
|
+
enableFile: true // 是否输出到文件
|
|
295
314
|
}</code></pre>
|
|
315
|
+
<h3>日志文件说明</h3>
|
|
316
|
+
<ul>
|
|
317
|
+
<li><code>YYYY-MM-DD.log</code>: 通用应用日志</li>
|
|
318
|
+
<li><code>YYYY-MM-DD.access.log</code>: 请求访问日志(JSON 行格式)</li>
|
|
319
|
+
<li><code>YYYY-MM-DD.error.log</code>: 错误级别日志</li>
|
|
320
|
+
</ul>
|
|
321
|
+
<h3>请求追踪(x-request-id)</h3>
|
|
322
|
+
<p>每个请求都会自动记录 <code>requestId</code>:</p>
|
|
323
|
+
<ul>
|
|
324
|
+
<li>请求头带有 <code>x-request-id</code> 时会透传该值</li>
|
|
325
|
+
<li>未提供时服务端会自动生成并在响应头返回</li>
|
|
326
|
+
<li>访问日志与错误日志都会带上 <code>requestId</code>,便于串联排查</li>
|
|
327
|
+
</ul>
|
|
296
328
|
</div>
|
|
297
329
|
|
|
298
330
|
<!-- 开发指南 -->
|