index.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. /**
  2. * 根据 Swagger json 自动生成接口服务文件
  3. */
  4. const config = require('./config');
  5. // 输出文件及目录定义
  6. const ROOT_PATH = './src/services';
  7. const ENUM_FILE = `${ROOT_PATH}/enums.ts`;
  8. const TYPING_D = `${ROOT_PATH}/typing.d.ts`;
  9. const API_PATH = `${ROOT_PATH}/apis`;
  10. // JS关键字
  11. const JS_KEY_WORDS = [
  12. 'break', 'else', 'new', 'var', 'case', 'finally', 'return', 'void', 'catch', 'for', 'switch',
  13. 'while', 'continue', 'function', 'this', 'with', 'default', 'if', 'throw', 'delete', 'in',
  14. 'try', 'do', 'instanceof', 'typeof', 'abstract', 'enum', 'int', 'short', 'boolean', 'export',
  15. 'interface', 'static', 'byte', 'extends', 'long', 'super', 'char', 'final', 'native', 'synchronized',
  16. 'class', 'float', 'package', 'throws', 'const', 'goto', 'private ', 'transient', 'debugger',
  17. 'implements', 'protected ', 'volatile', 'double', 'import', 'public',
  18. ];
  19. /** 数值类型映射 */
  20. const IntegerMapping = {
  21. decimal: 'number',
  22. int16: 'number',
  23. int32: 'number',
  24. int64: 'string',
  25. };
  26. /** 返回结果类型映射 */
  27. const ResultMapping = {
  28. Decimal: 'number',
  29. Int16: 'number',
  30. Int32: 'number',
  31. Int64: 'string',
  32. Boolean: 'boolean',
  33. Object: 'any',
  34. String: 'string',
  35. DateTime: 'string',
  36. FileContentResult: 'FileContentResult',
  37. };
  38. /** 统一注释 */
  39. const UnifyComment = `// @ts-ignore\n/* eslint-disable */\n\n// 该文件自动生成,请勿手动修改!`;
  40. // ----------------------------------------
  41. // 执行外部命令
  42. const execSync = require('child_process').execSync;
  43. // 文件操作
  44. const fs = require('fs');
  45. // ----------------------------------------
  46. /**
  47. * 下划线中横线分隔字符串转换为驼峰格式
  48. * @param {string} value 待转换字符串
  49. * @param {boolean} [capitalize] 首字母大写
  50. * @returns
  51. */
  52. const kebabToCamelCase = (value, capitalize = false) => {
  53. let n = value.replace(/[_-][a-zA-z]/g, (str) => str.substr(-1).toUpperCase()).trim() || '';
  54. if (capitalize && n.length != 0) {
  55. n = n.slice(0, 1).toUpperCase() + n.slice(1);
  56. }
  57. return n;
  58. };
  59. /**
  60. * 根据路径生成操作名称
  61. * @param {string} path 接口路径
  62. * @returns 取路径最后一级的名称
  63. */
  64. const getActionName = (path) => {
  65. const pp = path.split('/');
  66. if (pp.length == 0) {
  67. return '';
  68. }
  69. const n = pp[pp.length - 1];
  70. let an = kebabToCamelCase(n);
  71. if (JS_KEY_WORDS.includes(an.toLocaleLowerCase())) {
  72. an = `${an}Action`;
  73. }
  74. return an;
  75. };
  76. /**
  77. * 生成控制器名称
  78. * @param {string} name Swagger的tag名称
  79. * @returns
  80. */
  81. const getControllerName = (name) => {
  82. const n = kebabToCamelCase(name, true);
  83. return `${n}Controller`;
  84. };
  85. /**
  86. * 清除文件
  87. * @param {*} dir 需要清空的目录
  88. */
  89. const clearFiles = (dir) => {
  90. const oldFiles = fs.readdirSync(dir);
  91. oldFiles.forEach((f) => {
  92. const filePath = `${dir}/${f}`;
  93. const s = fs.statSync(filePath);
  94. if (s.isDirectory()) {
  95. return;
  96. }
  97. fs.unlinkSync(filePath);
  98. });
  99. };
  100. /** 获取引用类型 */
  101. const getRefEntity = ($ref) => {
  102. const rs = $ref.split('/');
  103. if (rs.length == 0) {
  104. return 'any';
  105. }
  106. return rs[rs.length - 1];
  107. };
  108. /** 获取简单类型 */
  109. const getSimpleType = ({ type, format }) => {
  110. if (!type) {
  111. return 'any';
  112. }
  113. const ltype = type.toLowerCase();
  114. if (['string', 'boolean', 'number'].includes(type)) {
  115. return ltype;
  116. }
  117. if (ltype == 'integer') {
  118. return IntegerMapping[format] ?? 'number';
  119. }
  120. return 'any';
  121. };
  122. /** 获取响应结果类型 */
  123. const getResultType = (type) => {
  124. const t = ResultMapping[type];
  125. if (t) {
  126. return t;
  127. }
  128. return `API.${type}`;
  129. };
  130. /** 获取实体类型 */
  131. const getEntityType = (name, schema) => {
  132. const props = schema.properties;
  133. let entity = '';
  134. const desc = schema.description;
  135. if (desc) {
  136. entity = `/** ${desc} */\n`;
  137. }
  138. entity = `${entity}type ${name} = {\n`;
  139. const requiredList = schema.required ?? [];
  140. for (const pname in props) {
  141. const prop = props[pname];
  142. let member = '';
  143. const memberDesc = prop.description;
  144. if (memberDesc) {
  145. member = `/** ${memberDesc} */\n`;
  146. }
  147. let memberType = '';
  148. if (prop['$ref']) {
  149. memberType = `${getRefEntity(prop['$ref'])}`;
  150. } else if (prop['items']) {
  151. if (prop['items']['$ref']) {
  152. memberType = `${getRefEntity(prop['items']['$ref'])}[]`;
  153. } else {
  154. memberType = `${getSimpleType(prop['items'])}[]`;
  155. }
  156. } else {
  157. memberType = `${getSimpleType(prop)}`;
  158. }
  159. const required = requiredList.includes(pname) || (prop.nullable !== undefined && !prop.nullable);
  160. member = `${member}${pname}${required ? '' : '?'}: ${memberType};\n`;
  161. entity = `${entity}${member}`;
  162. }
  163. entity = `${entity}}\n`;
  164. return entity;
  165. };
  166. /** 获取请求类型 */
  167. const getRequestBodyType = (reqBody) => {
  168. if (reqBody['content']['application/json']) {
  169. const schema = reqBody['content']['application/json']['schema'];
  170. let tps = '';
  171. if (schema['$ref']) {
  172. tps = getRefEntity(schema['$ref']);
  173. } else if (schema['items']) {
  174. if (schema['items']['$ref']) {
  175. tps = getRefEntity(schema['items']['$ref']);
  176. } else {
  177. tps = getSimpleType(schema['items']);
  178. }
  179. } else {
  180. tps = getSimpleType(schema);
  181. }
  182. if (schema['type'] == 'array') {
  183. return `${tps}[]`;
  184. }
  185. return tps;
  186. }
  187. if (reqBody['content']['multipart/form-data']) {
  188. return 'FormData';
  189. }
  190. return 'any';
  191. };
  192. /** 获取统一返回类型 */
  193. const getXnRestfulResultType = (propRef) => {
  194. const rtype = getRefEntity(propRef).replaceAll('XnRestfulResult_', '');
  195. const ps = rtype.split('_');
  196. if (ps.length == 1) {
  197. return getResultType(ps);
  198. }
  199. if (ps[0] == 'List') {
  200. return `${getResultType(rtype.replaceAll('List_', ''))}[]`;
  201. }
  202. if (ps[0] == 'PageResult') {
  203. return `API.PageResponse<${getResultType(rtype.replaceAll('PageResult_', ''))}>`;
  204. }
  205. return getResultType(rtype);
  206. };
  207. /** 获取响应类型 */
  208. const getResponseType = (res) => {
  209. const schema = res['200']?.['content']?.['application/json']?.['schema'];
  210. if (!schema) {
  211. return 'any';
  212. }
  213. if (schema['$ref']) {
  214. return getXnRestfulResultType(schema['$ref']);
  215. } else if (schema['items']) {
  216. if (schema['items']['$ref']) {
  217. return getXnRestfulResultType(schema['items']['$ref']);
  218. } else {
  219. return getSimpleType(schema['items']);
  220. }
  221. } else {
  222. return getSimpleType(schema);
  223. }
  224. };
  225. /** 获取查询参数类型 */
  226. const getParamsType = (parameters) => {
  227. let pb = '';
  228. for (const p of parameters) {
  229. const pdesc = p.description ?? p.name;
  230. pb = `${pb}/** ${pdesc} */\n${p.name}${p.required ? '' : '?'}:`;
  231. const prop = p.schema;
  232. let ptype = '';
  233. if (prop['$ref']) {
  234. ptype = `${getRefEntity(prop['$ref'])}`;
  235. } else if (prop['items']) {
  236. if (prop['items']['$ref']) {
  237. ptype = `${getRefEntity(prop['items']['$ref'])}[]`;
  238. } else {
  239. ptype = `${getSimpleType(prop['items'])}[]`;
  240. }
  241. } else {
  242. ptype = `${getSimpleType(prop)}`;
  243. }
  244. pb = `${pb}${ptype};\n`;
  245. }
  246. return `{\n${pb}}`;
  247. };
  248. /** 判断是否简单类型 */
  249. const isSimpleType = (type) => {
  250. return ['string', 'number', 'boolean', 'object', 'any', 'formdata'].includes(type.toLowerCase());
  251. };
  252. /** 获取 tag 描述字典 */
  253. const getTagDescDict = (tags) => {
  254. let tagDict = {};
  255. for (const tag of tags) {
  256. tagDict[tag.name] = tag.description ?? tag.name;
  257. }
  258. return tagDict;
  259. };
  260. /** 是否为文件响应类型 */
  261. const isFileResponse = (actionName, responseType) => {
  262. if (responseType == 'FileContentResult') {
  263. return true;
  264. }
  265. const an = actionName.toLowerCase();
  266. return an.includes('export') || an.includes('download');
  267. }
  268. // ----------------------------------------
  269. /** 生成枚举 */
  270. const generateEnums = (schemas) => {
  271. const enums = [];
  272. const importEnums = [];
  273. for (let ek in schemas) {
  274. if (schemas[ek].enum) {
  275. const desc = schemas[ek].description;
  276. if (!desc) {
  277. continue;
  278. }
  279. const s1 = desc.replaceAll('<br />', '').split('&nbsp;');
  280. if (s1 && s1.length > 1) {
  281. const enumComment = s1?.[0];
  282. const items = [];
  283. for (let j = 1; j < s1.length; j++) {
  284. const s2 = s1[j].split(' ');
  285. if (s2.length > 3) {
  286. const itemComent = s2[0];
  287. const itemCode = s2[1];
  288. const itemValue = s2[3];
  289. items.push(`\n /** ${itemComent} */\n ${itemCode} = ${itemValue},`);
  290. }
  291. }
  292. enums.push(`\n/** ${enumComment} */\nexport enum ${ek} {${items.join('')}\n}`);
  293. importEnums.push(ek);
  294. }
  295. }
  296. }
  297. const fs = require('fs');
  298. const es = enums.join('\n');
  299. const ies = importEnums.map((t) => ` ${t}`).join(',\n');
  300. fs.writeFileSync(ENUM_FILE, `${UnifyComment}\n\n${es}\n\nexport default {\n${ies},\n};\n`);
  301. execSync(`prettier --write ${ENUM_FILE}`);
  302. return importEnums;
  303. };
  304. /** 生成类型定义文件 */
  305. const generateEntity = (schemas, enums) => {
  306. // 生成类型
  307. let entities = [];
  308. for (const name in schemas) {
  309. const schema = schemas[name];
  310. if (
  311. name.includes('XnRestfulResult_') ||
  312. name.includes('PageResult_') ||
  313. schema.enum ||
  314. !schema.properties
  315. ) {
  316. continue;
  317. }
  318. const entity = getEntityType(name, schema);
  319. entities.push(entity);
  320. }
  321. const fs = require('fs');
  322. const ies = enums.map((t) => ` ${t}`).join(',\n');
  323. const es = entities.join('\n');
  324. fs.writeFileSync(
  325. TYPING_D,
  326. `${UnifyComment}
  327. import {
  328. ${ies},
  329. } from './enums';
  330. declare global {
  331. declare namespace API {
  332. /** 分页查询请求参数基类型 */
  333. type PageQueryType = {
  334. /** 当前页值 */
  335. pageIndex: number;
  336. /** 每页大小 */
  337. pageSize: number;
  338. /** 搜索值 */
  339. searchValue?: string;
  340. /** 搜索开始时间 */
  341. searchBeginTime?: string;
  342. /** 搜索结束时间 */
  343. searchEndTime?: string;
  344. /** 排序字段 */
  345. sortField?: string;
  346. /** 排序方法,默认升序,否则降序(配合antd前端,约定参数为 Ascend,Dscend) */
  347. sortOrder?: string;
  348. /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
  349. descStr?: string;
  350. /** 复杂查询条件 */
  351. searchParameters?: Condition[];
  352. };
  353. /** 后端服务请求返回参数 */
  354. type ResponseType<T = any> = {
  355. /** 执行成功 */
  356. success?: boolean;
  357. /** 状态码 */
  358. code?: number;
  359. /** 错误码 */
  360. errorCode?: number;
  361. /** 错误信息 */
  362. errorMessage?: any;
  363. /** 消息显示类型 */
  364. showType?: number;
  365. /** 数据 */
  366. data?: T;
  367. /** 附加数据 */
  368. extras?: any;
  369. /** 时间戳 */
  370. timestamp?: number;
  371. };
  372. /** 分页数据对象 */
  373. type PageResponse<T = any> = {
  374. /** 当前页值 */
  375. pageIndex: number;
  376. /** 每页大小 */
  377. pageSize: number;
  378. /** 数据总数 */
  379. totalCount: number;
  380. /** 总页数 */
  381. totalPage?: number;
  382. /** 数据行 */
  383. items?: T[];
  384. };
  385. /** 文件响应类型 */
  386. type FileResponse = {
  387. /** 文件名称 */
  388. fileName: string;
  389. /** 文件体 */
  390. data: Blob;
  391. };
  392. ${es}}
  393. }`,
  394. );
  395. execSync(`prettier --write ${TYPING_D}`);
  396. };
  397. /** 生成控制器文件 */
  398. const generateController = (paths, tagDict, enums) => {
  399. // 构造控制器
  400. let controllers = {};
  401. for (const apiPath in paths) {
  402. const actionName = getActionName(apiPath);
  403. const api = paths[apiPath];
  404. for (const verb in api) {
  405. // 只处理 post 和 get,其它忽略
  406. if (!['post', 'get'].includes(verb)) {
  407. continue;
  408. }
  409. const action = api[verb];
  410. const actionDesc = action.summary ?? actionName;
  411. // tag 作为控制器名称,如果没有 tag 则跳过
  412. const tags = action.tags ?? [];
  413. if (tags.length < 1) {
  414. continue;
  415. }
  416. for (const tag of tags) {
  417. const controllerName = getControllerName(tag);
  418. if (!controllers.hasOwnProperty(controllerName)) {
  419. controllers[controllerName] = {
  420. description: tagDict[tag],
  421. actions: [],
  422. actionNames: [],
  423. enums: [],
  424. blobActions: [],
  425. };
  426. }
  427. let paramsType = '';
  428. let dataType = '';
  429. let responseType = '';
  430. if (action.parameters) {
  431. paramsType = getParamsType(action.parameters);
  432. }
  433. if (action.requestBody) {
  434. dataType = getRequestBodyType(action.requestBody);
  435. }
  436. if (action.responses) {
  437. responseType = getResponseType(action.responses);
  438. }
  439. const es = enums.filter(t => paramsType.indexOf(t) !== -1 || dataType.indexOf(t) !== -1 || responseType.indexOf(t) !== -1);
  440. if (es.length > 0) {
  441. controllers[controllerName].enums.push(es[0]);
  442. }
  443. const actionFullDesc = `/** ${actionDesc} ${verb.toUpperCase()} ${apiPath} */\n`;
  444. let actionBody = actionFullDesc;
  445. // file response api
  446. if (isFileResponse(actionName, responseType)) {
  447. controllers[controllerName].blobActions.push(actionName);
  448. actionBody = `${actionFullDesc}export async function ${actionName}(`;
  449. if (verb == 'get') {
  450. if (paramsType != '') {
  451. actionBody = `${actionBody}params:${paramsType},`;
  452. }
  453. } else {
  454. if (paramsType != '') {
  455. actionBody = `${actionBody}params:${paramsType},`;
  456. }
  457. if (dataType != '') {
  458. const prefix = isSimpleType(dataType) ? '' : 'API.';
  459. actionBody = `${actionBody}data:${prefix}${dataType},`;
  460. }
  461. }
  462. actionBody = `${actionBody}options?:{[key:string]:any}){\n`;
  463. actionBody = `${actionBody}const url='${apiPath}';\n`;
  464. actionBody = `${actionBody}const config={method:'${verb.toUpperCase()}',`;
  465. if (verb == 'get' && paramsType != '') {
  466. actionBody = `${actionBody}params,`;
  467. }
  468. if (verb == 'post') {
  469. let pbv = '';
  470. if (paramsType != '') {
  471. pbv = 'params,';
  472. }
  473. if (dataType != '') {
  474. pbv = `${pbv}data,`;
  475. }
  476. actionBody = `${actionBody}${pbv}`;
  477. }
  478. actionBody = `${actionBody}...(options||{}), responseType: 'blob', getResponse: true} as RequestOptions;\n`;
  479. actionBody = `${actionBody}const res = await request(url, config);\n`;
  480. actionBody = `${actionBody}const hcd = res.request.getResponseHeader('Content-Disposition');\n`;
  481. actionBody = `${actionBody}if(!hcd){ return null; }\n`;
  482. actionBody = `${actionBody}let fileName = '';\n`;
  483. actionBody = `${actionBody}const cd = contentDisposition.parse(hcd)\n`;
  484. actionBody = `${actionBody}if (cd?.parameters?.filename) { fileName = cd?.parameters?.filename; }\n`;
  485. actionBody = `${actionBody}return { fileName, data:res.data as Blob } as API.FileResponse;\n}\n`;
  486. }
  487. // other api
  488. else {
  489. actionBody = `${actionFullDesc}export async function ${actionName}(`;
  490. if (verb == 'get') {
  491. if (paramsType != '') {
  492. actionBody = `${actionBody}params:${paramsType},`;
  493. }
  494. } else {
  495. if (paramsType != '') {
  496. actionBody = `${actionBody}params:${paramsType},`;
  497. }
  498. if (dataType != '') {
  499. const prefix = isSimpleType(dataType) ? '' : 'API.';
  500. actionBody = `${actionBody}data:${prefix}${dataType},`;
  501. }
  502. }
  503. actionBody = `${actionBody}options?:{[key:string]:any}){\n`;
  504. actionBody = `${actionBody}const url='${apiPath}';\n`;
  505. actionBody = `${actionBody}const config={method:'${verb.toUpperCase()}',`;
  506. if (verb == 'get' && paramsType != '') {
  507. actionBody = `${actionBody}params,`;
  508. }
  509. if (verb == 'post') {
  510. let pbv = '';
  511. if (paramsType != '') {
  512. pbv = 'params,';
  513. }
  514. if (dataType != '') {
  515. pbv = `${pbv}data,`;
  516. }
  517. actionBody = `${actionBody}${pbv}`;
  518. }
  519. actionBody = `${actionBody}...(options||{})};\n`;
  520. actionBody = `${actionBody}const res = await request<API.ResponseType<${responseType}>>(url, config);`;
  521. actionBody = `${actionBody}return res?.data;\n}\n`;
  522. }
  523. controllers[controllerName].actions.push(actionBody);
  524. controllers[controllerName].actionNames.push(`${actionFullDesc}${actionName},\n`);
  525. }
  526. }
  527. }
  528. // 生成控制器文件
  529. let csnames = [];
  530. for (let ck in controllers) {
  531. const c = controllers[ck];
  532. const es = [...new Set(c.enums)].map((t) => `${t}, `);
  533. let importEnums = '';
  534. if (es.length > 0) {
  535. importEnums = `\nimport { ${es.join('')} } from '../enums';`;
  536. }
  537. csnames.push({ name: ck, description: c.description });
  538. const controllerFile = `${API_PATH}/${ck}.ts`;
  539. const hasBlob = c.blobActions.length > 0;
  540. fs.writeFileSync(
  541. controllerFile,
  542. `${UnifyComment}
  543. // --------------------------------------------------------------------------
  544. // ${c.description}
  545. // --------------------------------------------------------------------------
  546. import { request${hasBlob ? ', RequestOptions' : ''} } from '@umijs/max';${importEnums}
  547. ${hasBlob ? 'import contentDisposition from \'content-disposition\';' : ''}
  548. ${c.actions.join('\n')}
  549. export default {
  550. ${c.actionNames.join('')}
  551. };
  552. `,
  553. );
  554. execSync(`prettier --write ${controllerFile}`);
  555. }
  556. // 生成index.ts
  557. const ecs = csnames.map((t) => `import * as ${t.name} from './${t.name}';\n`);
  558. const edefaults = csnames.map((t) => `/** ${t.description} */\n${t.name},\n`);
  559. const indexFile = `${API_PATH}/index.ts`;
  560. fs.writeFileSync(
  561. indexFile,
  562. `${UnifyComment}
  563. ${ecs.join('')}
  564. export default {
  565. ${edefaults.join('')}
  566. };
  567. `,
  568. );
  569. execSync(`prettier --write ${indexFile}`);
  570. };
  571. // ----------------------------------------
  572. /** 生成接口服务 */
  573. const generate = (error, response, body) => {
  574. // OpenApi 描述文件
  575. const swagger = JSON.parse(body);
  576. // 实体模型
  577. const schemas = swagger['components']['schemas'];
  578. // API路径
  579. const apiPaths = swagger['paths'];
  580. // 开始处理
  581. console.info('生成后台接口文件');
  582. console.info(`-----------------\n${new Date().toLocaleString()}\n-----------------\n清理文件`);
  583. clearFiles(ROOT_PATH);
  584. if (!fs.existsSync(API_PATH)) {
  585. fs.mkdirSync(API_PATH);
  586. } else {
  587. clearFiles(API_PATH);
  588. }
  589. console.info('清理完成!');
  590. console.log('-----------------\n开始生成');
  591. // 生成枚举文件
  592. console.log('> 生成枚举文件');
  593. const enums = generateEnums(schemas);
  594. // 生成实体类型文件
  595. console.log('> 生成实体类型文件');
  596. generateEntity(schemas, enums);
  597. // 生成控制器文件
  598. console.log('> 生成控制器文件');
  599. generateController(apiPaths, getTagDescDict(swagger['tags'] ?? []), enums);
  600. console.info('生成完成!');
  601. };
  602. const request = require('request');
  603. request.get(config.swaggerJson, generate);