czh-api 1.0.0 → 1.0.1
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/CHANGELOG.md +26 -0
- package/README.md +575 -1
- package/dist/commands/build.js +2 -0
- package/package.json +1 -1
- package/src/commands/build.ts +3 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# 更新日志
|
|
2
|
+
|
|
3
|
+
所有重要的版本更新都会记录在此文件中。
|
|
4
|
+
|
|
5
|
+
## [1.0.1] - 2025-12-25
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
- 添加 Handlebars `eq` helper,支持在模板中进行条件判断
|
|
9
|
+
- 模板中可使用 `{{#if (eq method "delete")}}del{{else}}{{method}}{{/if}}` 语法
|
|
10
|
+
|
|
11
|
+
### 修复
|
|
12
|
+
- 修复 chalk v5 ESM 兼容性问题,降级至 chalk v4
|
|
13
|
+
|
|
14
|
+
## [1.0.0] - 2025-07-02
|
|
15
|
+
|
|
16
|
+
### 新增
|
|
17
|
+
- 初始版本发布
|
|
18
|
+
- 支持 Swagger v2、OpenAPI v3、Knife4j 风格文档
|
|
19
|
+
- 根据 `x-package` 字段智能模块分组
|
|
20
|
+
- API 函数名采用 `HTTP方法` + `驼峰式路径` 命名
|
|
21
|
+
- 自定义 Handlebars 模板支持
|
|
22
|
+
- 自动生成 TypeScript 类型定义
|
|
23
|
+
- JSDoc 注释自动生成
|
|
24
|
+
- 支持 FormData / multipart 文件上传
|
|
25
|
+
- 可配置排除路径
|
|
26
|
+
- 可配置自定义导入语句
|
package/README.md
CHANGED
|
@@ -63,6 +63,88 @@ npm install -g czh-api
|
|
|
63
63
|
| `customImports` | `string[]` | 否 | 一个自定义导入语句的数组,会被添加到每个生成的 API 文件的顶部。 |
|
|
64
64
|
| `excludePaths` | `string[]` | 否 | 一个 URL 路径前缀的数组。任何以此数组中前缀开头的 API 都将被忽略。 |
|
|
65
65
|
|
|
66
|
+
## 📝 模板配置
|
|
67
|
+
|
|
68
|
+
`czh-api` 使用 Handlebars 模板引擎来生成代码。运行 `czh-api init` 后会在 `czh-api-template` 目录下生成默认模板,您可以根据项目需求自定义。
|
|
69
|
+
|
|
70
|
+
### 模板文件
|
|
71
|
+
|
|
72
|
+
| 文件名 | 用途 |
|
|
73
|
+
|--------------|--------------------------------|
|
|
74
|
+
| `api.hbs` | 生成每个 API 函数的代码 |
|
|
75
|
+
| `types.hbs` | 生成类型定义文件(如有) |
|
|
76
|
+
| `index.hbs` | 生成模块索引文件(如有) |
|
|
77
|
+
|
|
78
|
+
### 可用变量
|
|
79
|
+
|
|
80
|
+
在 `api.hbs` 模板中,您可以使用以下变量:
|
|
81
|
+
|
|
82
|
+
| 变量名 | 类型 | 描述 |
|
|
83
|
+
|------------------------|------------|------------------------------------------------|
|
|
84
|
+
| `functionName` | `string` | 生成的函数名,如 `postSysUserAdd` |
|
|
85
|
+
| `description` | `string` | 接口描述 |
|
|
86
|
+
| `method` | `string` | HTTP 方法:`get`, `post`, `put`, `delete` 等 |
|
|
87
|
+
| `path` | `string` | 请求路径,路径参数已转为模板字符串格式 |
|
|
88
|
+
| `hasParams` | `boolean` | 是否有查询/路径参数 |
|
|
89
|
+
| `hasData` | `boolean` | 是否有请求体 |
|
|
90
|
+
| `requestParamsTypeName`| `string` | 请求参数的类型名 |
|
|
91
|
+
| `requestBodyTypeName` | `string` | 请求体的类型名 |
|
|
92
|
+
| `responseTypeName` | `string` | 响应数据的类型名 |
|
|
93
|
+
| `contentType` | `string` | Content-Type,如 `multipart/form-data` |
|
|
94
|
+
| `jsdocParams` | `array` | JSDoc 参数列表,包含 `name`, `type`, `description`, `required` |
|
|
95
|
+
|
|
96
|
+
### 内置 Helpers
|
|
97
|
+
|
|
98
|
+
| Helper | 用法 | 描述 |
|
|
99
|
+
|--------|-----------------------------------|----------------|
|
|
100
|
+
| `eq` | `{{#if (eq method "delete")}}...{{/if}}` | 判断两个值是否相等 |
|
|
101
|
+
|
|
102
|
+
### 模板示例
|
|
103
|
+
|
|
104
|
+
默认的 `api.hbs` 模板:
|
|
105
|
+
|
|
106
|
+
```handlebars
|
|
107
|
+
/**
|
|
108
|
+
* @description {{description}}
|
|
109
|
+
{{#if jsdocParams}}
|
|
110
|
+
{{#each jsdocParams}}
|
|
111
|
+
* @param { {{this.type}} } {{#unless this.required}}[{{/unless}}{{this.name}}{{#unless this.required}}]{{/unless}} - {{this.description}}
|
|
112
|
+
{{/each}}
|
|
113
|
+
{{/if}}
|
|
114
|
+
*/
|
|
115
|
+
export const {{functionName}} = ({{#if hasParams}}params: {{requestParamsTypeName}}{{/if}}{{#if hasData}}{{#if hasParams}}, {{/if}}data: {{requestBodyTypeName}}{{/if}}) => {
|
|
116
|
+
return http.request<{{responseTypeName}}>({
|
|
117
|
+
url: `{{path}}`,
|
|
118
|
+
method: '{{method}}',
|
|
119
|
+
{{#if hasParams}}
|
|
120
|
+
params,
|
|
121
|
+
{{/if}}
|
|
122
|
+
{{#if hasData}}
|
|
123
|
+
data,
|
|
124
|
+
{{/if}}
|
|
125
|
+
{{#if contentType}}
|
|
126
|
+
headers: { 'Content-Type': '{{contentType}}' },
|
|
127
|
+
{{/if}}
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
如果您的 HTTP 客户端使用 `request.get()`, `request.post()` 这种风格,并且 `delete` 方法需要改为 `del`,可以这样写:
|
|
133
|
+
|
|
134
|
+
```handlebars
|
|
135
|
+
export const {{functionName}} = ({{#if hasParams}}params: {{requestParamsTypeName}}{{/if}}{{#if hasData}}{{#if hasParams}}, {{/if}}data: {{requestBodyTypeName}}{{/if}}) => {
|
|
136
|
+
return request.{{#if (eq method "delete")}}del{{else}}{{method}}{{/if}}<{{responseTypeName}}>({
|
|
137
|
+
url: `{{path}}`,
|
|
138
|
+
{{#if hasParams}}
|
|
139
|
+
params,
|
|
140
|
+
{{/if}}
|
|
141
|
+
{{#if hasData}}
|
|
142
|
+
data,
|
|
143
|
+
{{/if}}
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
```
|
|
147
|
+
|
|
66
148
|
|
|
67
149
|
## 👨💻 开发者指南
|
|
68
150
|
|
|
@@ -93,6 +175,13 @@ npm link
|
|
|
93
175
|
npm publish
|
|
94
176
|
```
|
|
95
177
|
|
|
178
|
+
发布到npm官方仓库需要切到官方源
|
|
179
|
+
```bash
|
|
180
|
+
npm config set registry https://registry.npmjs.org/
|
|
181
|
+
npm config set //registry.npmjs.org/:_authToken=你的token
|
|
182
|
+
npm publish
|
|
183
|
+
```
|
|
184
|
+
|
|
96
185
|
### 5. 解除本地链接/卸载
|
|
97
186
|
如果您想解除本地的链接状态,可以使用 `unlink` 命令。如果您想从全局卸载,请使用 `uninstall`。
|
|
98
187
|
```bash
|
|
@@ -101,4 +190,489 @@ npm unlink czh-api
|
|
|
101
190
|
|
|
102
191
|
# 或,全局卸载包
|
|
103
192
|
npm uninstall -g czh-api
|
|
104
|
-
```
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### 6. 如果服务端是knife4j风格的JSON文档
|
|
196
|
+
|
|
197
|
+
#### 6.1 SpringBoot3配置
|
|
198
|
+
SpringBoot3项目直接复制6.3配置文件即可
|
|
199
|
+
|
|
200
|
+
#### 6.2 SpringBoot2配置
|
|
201
|
+
复制6.3 把jakarta替换为javax
|
|
202
|
+
|
|
203
|
+
#### 6.3 SpringBoot配置
|
|
204
|
+
|
|
205
|
+
**ResponseWrapper.java**
|
|
206
|
+
|
|
207
|
+
```java
|
|
208
|
+
import jakarta.servlet.ServletOutputStream;
|
|
209
|
+
import jakarta.servlet.WriteListener;
|
|
210
|
+
import jakarta.servlet.http.HttpServletResponse;
|
|
211
|
+
import jakarta.servlet.http.HttpServletResponseWrapper;
|
|
212
|
+
|
|
213
|
+
import java.io.ByteArrayOutputStream;
|
|
214
|
+
import java.io.IOException;
|
|
215
|
+
import java.io.PrintWriter;
|
|
216
|
+
import java.io.StringWriter;
|
|
217
|
+
|
|
218
|
+
public class ResponseWrapper extends HttpServletResponseWrapper {
|
|
219
|
+
private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
|
220
|
+
private final StringWriter stringWriter = new StringWriter();
|
|
221
|
+
private PrintWriter writer;
|
|
222
|
+
private boolean writerUsed = false;
|
|
223
|
+
private boolean outputStreamUsed = false;
|
|
224
|
+
|
|
225
|
+
public ResponseWrapper(HttpServletResponse response) {
|
|
226
|
+
super(response);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
@Override
|
|
230
|
+
public PrintWriter getWriter() throws IOException {
|
|
231
|
+
if (outputStreamUsed) {
|
|
232
|
+
throw new IllegalStateException("getOutputStream() has already been called for this response");
|
|
233
|
+
}
|
|
234
|
+
if (writer == null) {
|
|
235
|
+
writer = new PrintWriter(stringWriter);
|
|
236
|
+
}
|
|
237
|
+
writerUsed = true;
|
|
238
|
+
return writer;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
@Override
|
|
242
|
+
public ServletOutputStream getOutputStream() throws IOException {
|
|
243
|
+
if (writerUsed) {
|
|
244
|
+
throw new IllegalStateException("getWriter() has already been called for this response");
|
|
245
|
+
}
|
|
246
|
+
outputStreamUsed = true;
|
|
247
|
+
return new ServletOutputStream() {
|
|
248
|
+
@Override
|
|
249
|
+
public boolean isReady() {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
@Override
|
|
254
|
+
public void setWriteListener(WriteListener writeListener) {
|
|
255
|
+
// Do nothing
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@Override
|
|
259
|
+
public void write(int b) throws IOException {
|
|
260
|
+
outputStream.write(b);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
public String getContent() {
|
|
266
|
+
if (writerUsed) {
|
|
267
|
+
writer.flush();
|
|
268
|
+
return stringWriter.toString();
|
|
269
|
+
} else if (outputStreamUsed) {
|
|
270
|
+
return outputStream.toString();
|
|
271
|
+
}
|
|
272
|
+
return "";
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**ApiDocsFormDataFilter.java**
|
|
278
|
+
|
|
279
|
+
```java
|
|
280
|
+
import com.alibaba.fastjson2.JSON;
|
|
281
|
+
import com.alibaba.fastjson2.JSONArray;
|
|
282
|
+
import com.alibaba.fastjson2.JSONObject;
|
|
283
|
+
import jakarta.annotation.PostConstruct;
|
|
284
|
+
import jakarta.servlet.*;
|
|
285
|
+
import jakarta.servlet.http.HttpServletRequest;
|
|
286
|
+
import jakarta.servlet.http.HttpServletResponse;
|
|
287
|
+
import org.springframework.beans.factory.annotation.Autowired;
|
|
288
|
+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
289
|
+
import org.springframework.core.annotation.Order;
|
|
290
|
+
import org.springframework.stereotype.Component;
|
|
291
|
+
import org.springframework.web.method.HandlerMethod;
|
|
292
|
+
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
|
293
|
+
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
|
294
|
+
|
|
295
|
+
import java.io.IOException;
|
|
296
|
+
import java.nio.charset.StandardCharsets;
|
|
297
|
+
import java.util.Arrays;
|
|
298
|
+
import java.util.Map;
|
|
299
|
+
import java.util.Set;
|
|
300
|
+
import java.util.concurrent.ConcurrentHashMap;
|
|
301
|
+
|
|
302
|
+
@Component
|
|
303
|
+
@Order(1)
|
|
304
|
+
public class ApiDocsFormDataFilter implements Filter {
|
|
305
|
+
|
|
306
|
+
@Autowired
|
|
307
|
+
private RequestMappingHandlerMapping requestMappingHandlerMapping;
|
|
308
|
+
|
|
309
|
+
// 缓存路径到Controller的映射关系
|
|
310
|
+
private final Map<String, String> pathToControllerMap = new ConcurrentHashMap<>();
|
|
311
|
+
|
|
312
|
+
// 特殊指定的formdata路径(文件上传等)
|
|
313
|
+
private static final Set<String> FORCE_FORM_DATA_PATHS = Set.of();
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* 初始化时自动扫描所有Controller映射
|
|
317
|
+
*/
|
|
318
|
+
@PostConstruct
|
|
319
|
+
public void initControllerMappings() {
|
|
320
|
+
try {
|
|
321
|
+
Map<RequestMappingInfo, HandlerMethod> handlerMethods =
|
|
322
|
+
requestMappingHandlerMapping.getHandlerMethods();
|
|
323
|
+
|
|
324
|
+
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethods.entrySet()) {
|
|
325
|
+
RequestMappingInfo mappingInfo = entry.getKey();
|
|
326
|
+
HandlerMethod handlerMethod = entry.getValue();
|
|
327
|
+
|
|
328
|
+
// 获取Controller类的完整名称
|
|
329
|
+
String controllerClass = handlerMethod.getBeanType().getName();
|
|
330
|
+
|
|
331
|
+
// 获取路径模式
|
|
332
|
+
Set<String> patterns = mappingInfo.getPatternValues();
|
|
333
|
+
for (String pattern : patterns) {
|
|
334
|
+
// 清理路径模式(移除路径变量)
|
|
335
|
+
String cleanPath = cleanPathPattern(pattern);
|
|
336
|
+
pathToControllerMap.put(cleanPath, controllerClass);
|
|
337
|
+
|
|
338
|
+
// 同时存储原始路径
|
|
339
|
+
pathToControllerMap.put(pattern, controllerClass);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
} catch (Exception e) {
|
|
344
|
+
e.printStackTrace();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* 清理路径模式,移除路径变量
|
|
350
|
+
*/
|
|
351
|
+
private String cleanPathPattern(String pattern) {
|
|
352
|
+
// 移除路径变量 {id} -> 空
|
|
353
|
+
return pattern.replaceAll("\\{[^}]+\\}", "");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
@Override
|
|
357
|
+
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
|
358
|
+
throws IOException, ServletException {
|
|
359
|
+
|
|
360
|
+
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
|
361
|
+
|
|
362
|
+
// 只处理 /v3/api-docs 请求
|
|
363
|
+
if (httpRequest.getRequestURI().contains("/v3/api-docs")) {
|
|
364
|
+
|
|
365
|
+
// 创建响应包装器来捕获原始响应
|
|
366
|
+
ResponseWrapper responseWrapper = new ResponseWrapper((HttpServletResponse) response);
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
// 继续执行原始请求
|
|
370
|
+
chain.doFilter(request, responseWrapper);
|
|
371
|
+
|
|
372
|
+
// 获取原始响应内容
|
|
373
|
+
String originalContent = responseWrapper.getContent();
|
|
374
|
+
|
|
375
|
+
// 智能标识请求类型
|
|
376
|
+
String modifiedContent = smartMarkRequestTypes(originalContent);
|
|
377
|
+
|
|
378
|
+
// 写入修改后的内容
|
|
379
|
+
response.setContentType("application/json;charset=UTF-8");
|
|
380
|
+
response.setContentLength(modifiedContent.getBytes(StandardCharsets.UTF_8).length);
|
|
381
|
+
response.getOutputStream().write(modifiedContent.getBytes(StandardCharsets.UTF_8));
|
|
382
|
+
response.getOutputStream().flush();
|
|
383
|
+
|
|
384
|
+
} catch (Exception e) {
|
|
385
|
+
e.printStackTrace();
|
|
386
|
+
// 如果出错,返回原始内容
|
|
387
|
+
chain.doFilter(request, response);
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
chain.doFilter(request, response);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private String smartMarkRequestTypes(String originalContent) {
|
|
395
|
+
try {
|
|
396
|
+
// 使用 fastjson2 解析
|
|
397
|
+
JSONObject rootJson = JSON.parseObject(originalContent);
|
|
398
|
+
|
|
399
|
+
// 处理所有路径
|
|
400
|
+
JSONObject paths = rootJson.getJSONObject("paths");
|
|
401
|
+
if (paths != null) {
|
|
402
|
+
processAllPathsSmart(paths);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return rootJson.toJSONString();
|
|
406
|
+
} catch (Exception e) {
|
|
407
|
+
e.printStackTrace();
|
|
408
|
+
return originalContent;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private void processAllPathsSmart(JSONObject paths) {
|
|
413
|
+
for (String pathKey : paths.keySet()) {
|
|
414
|
+
JSONObject pathItem = paths.getJSONObject(pathKey);
|
|
415
|
+
if (pathItem != null) {
|
|
416
|
+
processPathItemSmart(pathKey, pathItem);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private void processPathItemSmart(String pathKey, JSONObject pathItem) {
|
|
422
|
+
// 检查所有HTTP方法
|
|
423
|
+
Arrays.asList("get", "post", "put", "delete", "patch").forEach(method -> {
|
|
424
|
+
JSONObject operation = pathItem.getJSONObject(method);
|
|
425
|
+
if (operation != null) {
|
|
426
|
+
smartProcessOperation(operation, pathKey, method);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private void smartProcessOperation(JSONObject operation, String pathKey, String method) {
|
|
432
|
+
String requestType = "params"; // 默认为params
|
|
433
|
+
String contentType = null;
|
|
434
|
+
|
|
435
|
+
// 1. 强制指定的formdata路径
|
|
436
|
+
if (FORCE_FORM_DATA_PATHS.contains(pathKey)) {
|
|
437
|
+
requestType = "formdata";
|
|
438
|
+
contentType = "multipart/form-data";
|
|
439
|
+
convertToFormData(operation);
|
|
440
|
+
}
|
|
441
|
+
// 2. 检查是否有 requestBody (对应 @RequestBody)
|
|
442
|
+
else {
|
|
443
|
+
JSONObject requestBody = operation.getJSONObject("requestBody");
|
|
444
|
+
if (requestBody != null) {
|
|
445
|
+
JSONObject content = requestBody.getJSONObject("content");
|
|
446
|
+
if (content != null) {
|
|
447
|
+
// 检查各种 content-type
|
|
448
|
+
if (content.containsKey("application/json")) {
|
|
449
|
+
// @RequestBody + JSON
|
|
450
|
+
requestType = "json";
|
|
451
|
+
contentType = "application/json";
|
|
452
|
+
}
|
|
453
|
+
else if (content.containsKey("application/x-www-form-urlencoded")) {
|
|
454
|
+
// 表单数据 -> 转为 formdata
|
|
455
|
+
requestType = "formdata";
|
|
456
|
+
contentType = "multipart/form-data";
|
|
457
|
+
convertFormUrlencodedToFormData(content);
|
|
458
|
+
}
|
|
459
|
+
else if (content.containsKey("multipart/form-data")) {
|
|
460
|
+
// 已经是 formdata
|
|
461
|
+
requestType = "formdata";
|
|
462
|
+
contentType = "multipart/form-data";
|
|
463
|
+
}
|
|
464
|
+
// 检查是否包含文件上传
|
|
465
|
+
else if (hasFileUpload(content) || isFileUploadPath(pathKey)) {
|
|
466
|
+
requestType = "formdata";
|
|
467
|
+
contentType = "multipart/form-data";
|
|
468
|
+
convertToFormData(operation);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// 3. 没有 requestBody,检查是否有查询参数
|
|
473
|
+
else {
|
|
474
|
+
JSONArray parameters = operation.getJSONArray("parameters");
|
|
475
|
+
if (parameters != null) {
|
|
476
|
+
boolean hasQueryParams = false;
|
|
477
|
+
for (int i = 0; i < parameters.size(); i++) {
|
|
478
|
+
JSONObject param = parameters.getJSONObject(i);
|
|
479
|
+
if (param != null && "query".equals(param.getString("in"))) {
|
|
480
|
+
hasQueryParams = true;
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (hasQueryParams) {
|
|
485
|
+
requestType = "params";
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// 4. 根据路径和方法推断
|
|
490
|
+
if (isFileUploadPath(pathKey)) {
|
|
491
|
+
requestType = "formdata";
|
|
492
|
+
contentType = "multipart/form-data";
|
|
493
|
+
} else if ("post".equals(method) || "put".equals(method) || "patch".equals(method)) {
|
|
494
|
+
// POST/PUT/PATCH 但没有 requestBody,可能是表单提交
|
|
495
|
+
requestType = "formdata";
|
|
496
|
+
contentType = "multipart/form-data";
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// 添加自定义扩展字段
|
|
502
|
+
operation.put("x-request-type", requestType);
|
|
503
|
+
if (contentType != null) {
|
|
504
|
+
operation.put("x-content-type", contentType);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// 🆕 自动获取Controller包路径
|
|
508
|
+
String controllerClass = getControllerClassAuto(pathKey);
|
|
509
|
+
if (controllerClass != null) {
|
|
510
|
+
operation.put("x-package", controllerClass);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* 自动获取Controller类路径(从缓存的映射中查找)
|
|
516
|
+
*/
|
|
517
|
+
private String getControllerClassAuto(String pathKey) {
|
|
518
|
+
// 1. 直接匹配
|
|
519
|
+
String controllerClass = pathToControllerMap.get(pathKey);
|
|
520
|
+
if (controllerClass != null) {
|
|
521
|
+
return controllerClass;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// 2. 模糊匹配(处理路径变量的情况)
|
|
525
|
+
for (Map.Entry<String, String> entry : pathToControllerMap.entrySet()) {
|
|
526
|
+
String mappedPath = entry.getKey();
|
|
527
|
+
String mappedController = entry.getValue();
|
|
528
|
+
|
|
529
|
+
// 检查是否是路径变量匹配
|
|
530
|
+
if (isPathMatch(pathKey, mappedPath)) {
|
|
531
|
+
return mappedController;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// 3. 前缀匹配
|
|
536
|
+
String longestMatch = "";
|
|
537
|
+
String bestController = null;
|
|
538
|
+
for (Map.Entry<String, String> entry : pathToControllerMap.entrySet()) {
|
|
539
|
+
String mappedPath = entry.getKey();
|
|
540
|
+
String mappedController = entry.getValue();
|
|
541
|
+
|
|
542
|
+
// 去除路径变量后进行前缀匹配
|
|
543
|
+
String cleanMappedPath = cleanPathPattern(mappedPath);
|
|
544
|
+
String cleanPathKey = cleanPathPattern(pathKey);
|
|
545
|
+
|
|
546
|
+
if (cleanPathKey.startsWith(cleanMappedPath) && cleanMappedPath.length() > longestMatch.length()) {
|
|
547
|
+
longestMatch = cleanMappedPath;
|
|
548
|
+
bestController = mappedController;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return bestController;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* 检查路径是否匹配(支持路径变量)
|
|
557
|
+
*/
|
|
558
|
+
private boolean isPathMatch(String actualPath, String patternPath) {
|
|
559
|
+
// 简单的路径变量匹配
|
|
560
|
+
String[] actualParts = actualPath.split("/");
|
|
561
|
+
String[] patternParts = patternPath.split("/");
|
|
562
|
+
|
|
563
|
+
if (actualParts.length != patternParts.length) {
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
for (int i = 0; i < actualParts.length; i++) {
|
|
568
|
+
String actualPart = actualParts[i];
|
|
569
|
+
String patternPart = patternParts[i];
|
|
570
|
+
|
|
571
|
+
// 如果是路径变量,跳过
|
|
572
|
+
if (patternPart.startsWith("{") && patternPart.endsWith("}")) {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// 必须完全匹配
|
|
577
|
+
if (!actualPart.equals(patternPart)) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return true;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ... 其他方法保持不变 ...
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* 将 application/x-www-form-urlencoded 转换为 multipart/form-data
|
|
589
|
+
*/
|
|
590
|
+
private void convertFormUrlencodedToFormData(JSONObject content) {
|
|
591
|
+
if (content.containsKey("application/x-www-form-urlencoded")) {
|
|
592
|
+
Object formContent = content.get("application/x-www-form-urlencoded");
|
|
593
|
+
content.remove("application/x-www-form-urlencoded");
|
|
594
|
+
content.put("multipart/form-data", formContent);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* 将 application/json 转换为 multipart/form-data
|
|
600
|
+
*/
|
|
601
|
+
private void convertToFormData(JSONObject operation) {
|
|
602
|
+
JSONObject requestBody = operation.getJSONObject("requestBody");
|
|
603
|
+
if (requestBody != null) {
|
|
604
|
+
JSONObject content = requestBody.getJSONObject("content");
|
|
605
|
+
if (content != null && content.containsKey("application/json")) {
|
|
606
|
+
Object jsonContent = content.get("application/json");
|
|
607
|
+
content.remove("application/json");
|
|
608
|
+
content.put("multipart/form-data", jsonContent);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* 检查是否包含文件上传字段
|
|
615
|
+
*/
|
|
616
|
+
private boolean hasFileUpload(JSONObject content) {
|
|
617
|
+
// 检查各种 content-type 中是否有文件字段
|
|
618
|
+
for (String contentType : content.keySet()) {
|
|
619
|
+
JSONObject typeContent = content.getJSONObject(contentType);
|
|
620
|
+
if (typeContent != null) {
|
|
621
|
+
JSONObject schema = typeContent.getJSONObject("schema");
|
|
622
|
+
if (schema != null && checkSchemaForFiles(schema)) {
|
|
623
|
+
return true;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* 检查 schema 中是否包含文件字段
|
|
632
|
+
*/
|
|
633
|
+
private boolean checkSchemaForFiles(JSONObject schema) {
|
|
634
|
+
JSONObject properties = schema.getJSONObject("properties");
|
|
635
|
+
if (properties != null) {
|
|
636
|
+
for (String fieldName : properties.keySet()) {
|
|
637
|
+
JSONObject field = properties.getJSONObject(fieldName);
|
|
638
|
+
if (field != null) {
|
|
639
|
+
// 检查是否为文件类型
|
|
640
|
+
if ("string".equals(field.getString("type")) && "binary".equals(field.getString("format"))) {
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// 检查字段名是否包含文件相关关键词
|
|
645
|
+
// if (fieldName.toLowerCase().contains("file") ||
|
|
646
|
+
// fieldName.toLowerCase().contains("upload") ||
|
|
647
|
+
// fieldName.toLowerCase().contains("image") ||
|
|
648
|
+
// fieldName.toLowerCase().contains("document") ||
|
|
649
|
+
// fieldName.toLowerCase().contains("attachment")) {
|
|
650
|
+
// return true;
|
|
651
|
+
// }
|
|
652
|
+
|
|
653
|
+
// 检查数组类型的文件
|
|
654
|
+
if ("array".equals(field.getString("type"))) {
|
|
655
|
+
JSONObject items = field.getJSONObject("items");
|
|
656
|
+
if (items != null && "string".equals(items.getString("type")) && "binary".equals(items.getString("format"))) {
|
|
657
|
+
return true;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* 根据路径判断是否为文件上传接口
|
|
668
|
+
*/
|
|
669
|
+
private boolean isFileUploadPath(String pathKey) {
|
|
670
|
+
return pathKey.contains("/upload") ||
|
|
671
|
+
pathKey.contains("/file") ||
|
|
672
|
+
pathKey.contains("/image") ||
|
|
673
|
+
pathKey.contains("/document") ||
|
|
674
|
+
pathKey.contains("/attachment");
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
```
|
|
678
|
+
|
package/dist/commands/build.js
CHANGED
|
@@ -64,6 +64,8 @@ const handleBuild = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
64
64
|
return fs_1.default.readFileSync(defaultPath, 'utf-8');
|
|
65
65
|
}
|
|
66
66
|
};
|
|
67
|
+
// 注册 Handlebars helpers
|
|
68
|
+
handlebars_1.default.registerHelper('eq', (a, b) => a === b);
|
|
67
69
|
const apiTemplateStr = readTemplate(config.templates, 'api.hbs');
|
|
68
70
|
const apiTemplate = handlebars_1.default.compile(apiTemplateStr);
|
|
69
71
|
for (const moduleName in modules) {
|
package/package.json
CHANGED
package/src/commands/build.ts
CHANGED
|
@@ -65,6 +65,9 @@ export const handleBuild = async () => {
|
|
|
65
65
|
return fs.readFileSync(defaultPath, 'utf-8');
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
+
|
|
69
|
+
// 注册 Handlebars helpers
|
|
70
|
+
Handlebars.registerHelper('eq', (a, b) => a === b);
|
|
68
71
|
|
|
69
72
|
const apiTemplateStr = readTemplate(config.templates, 'api.hbs');
|
|
70
73
|
const apiTemplate = Handlebars.compile(apiTemplateStr);
|