index.js 22 KB

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