index.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  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(rtype.replaceAll('List_', ''))}[]`;
  198. }
  199. if (ps[0] == 'PageResult') {
  200. return `API.PageResponse<${getResultType(rtype.replaceAll('PageResult_', ''))}>`;
  201. }
  202. return getResultType(rtype);
  203. };
  204. /** 获取响应类型 */
  205. const getResponseType = (res) => {
  206. const schema = res['200']?.['content']?.['application/json']?.['schema'];
  207. if (!schema) {
  208. return 'any';
  209. }
  210. if (schema['$ref']) {
  211. return getXnRestfulResultType(schema['$ref']);
  212. } else if (schema['items']) {
  213. if (schema['items']['$ref']) {
  214. return getXnRestfulResultType(schema['items']['$ref']);
  215. } else {
  216. return getSimpleType(schema['items']);
  217. }
  218. } else {
  219. return getSimpleType(schema);
  220. }
  221. };
  222. /** 获取查询参数类型 */
  223. const getParamsType = (parameters) => {
  224. let pb = '';
  225. for (const p of parameters) {
  226. const pdesc = p.description ?? p.name;
  227. pb = `${pb}/** ${pdesc} */\n${p.name}${p.required ? '' : '?'}:`;
  228. const prop = p.schema;
  229. let ptype = '';
  230. if (prop['$ref']) {
  231. ptype = `${getRefEntity(prop['$ref'])}`;
  232. } else if (prop['items']) {
  233. if (prop['items']['$ref']) {
  234. ptype = `${getRefEntity(prop['items']['$ref'])}[]`;
  235. } else {
  236. ptype = `${getSimpleType(prop['items'])}[]`;
  237. }
  238. } else {
  239. ptype = `${getSimpleType(prop)}`;
  240. }
  241. pb = `${pb}${ptype};\n`;
  242. }
  243. return `{\n${pb}}`;
  244. };
  245. /** 判断是否简单类型 */
  246. const isSimpleType = (type) => {
  247. return ['string', 'number', 'boolean', 'object', 'any', 'formdata'].includes(type.toLowerCase());
  248. };
  249. /** 获取 tag 描述字典 */
  250. const getTagDescDict = (tags) => {
  251. let tagDict = {};
  252. for (const tag of tags) {
  253. tagDict[tag.name] = tag.description ?? tag.name;
  254. }
  255. return tagDict;
  256. };
  257. // ----------------------------------------
  258. /** 生成枚举 */
  259. const generateEnums = (schemas) => {
  260. const enums = [];
  261. const importEnums = [];
  262. for (let ek in schemas) {
  263. if (schemas[ek].enum) {
  264. const desc = schemas[ek].description;
  265. if (!desc) {
  266. continue;
  267. }
  268. const s1 = desc.replaceAll('<br />', '').split('&nbsp;');
  269. if (s1 && s1.length > 1) {
  270. const enumComment = s1?.[0];
  271. const items = [];
  272. for (let j = 1; j < s1.length; j++) {
  273. const s2 = s1[j].split(' ');
  274. if (s2.length > 3) {
  275. const itemComent = s2[0];
  276. const itemCode = s2[1];
  277. const itemValue = s2[3];
  278. items.push(`\n /** ${itemComent} */\n ${itemCode} = ${itemValue},`);
  279. }
  280. }
  281. enums.push(`\n/** ${enumComment} */\nexport enum ${ek} {${items.join('')}\n}`);
  282. importEnums.push(ek);
  283. }
  284. }
  285. }
  286. const fs = require('fs');
  287. const es = enums.join('\n');
  288. const ies = importEnums.map((t) => ` ${t}`).join(',\n');
  289. fs.writeFileSync(ENUM_FILE, `${UnifyComment}\n\n${es}\n\nexport default {\n${ies},\n};\n`);
  290. execSync(`prettier --write ${ENUM_FILE}`);
  291. return importEnums;
  292. };
  293. /** 生成类型定义文件 */
  294. const generateEntity = (schemas, enums) => {
  295. // 生成类型
  296. let entities = [];
  297. for (const name in schemas) {
  298. const schema = schemas[name];
  299. if (
  300. name.includes('XnRestfulResult_') ||
  301. name.includes('PageResult_') ||
  302. schema.enum ||
  303. !schema.properties
  304. ) {
  305. continue;
  306. }
  307. // const entity = getEntityType(name.split('_')[0], schema);
  308. // const ns = name.split('_');
  309. // const entity = getEntityType(ns[ns.length - 1], schema);
  310. const entity = getEntityType(name, schema);
  311. entities.push(entity);
  312. }
  313. const fs = require('fs');
  314. const ies = enums.map((t) => ` ${t}`).join(',\n');
  315. const es = entities.join('\n');
  316. fs.writeFileSync(
  317. TYPING_D,
  318. `${UnifyComment}
  319. import {
  320. ${ies},
  321. } from './enums';
  322. declare global {
  323. declare namespace API {
  324. /** 分页查询请求参数基类型 */
  325. type PageQueryType = {
  326. /** 当前页值 */
  327. pageIndex: number;
  328. /** 每页大小 */
  329. pageSize: number;
  330. /** 搜索值 */
  331. searchValue?: string;
  332. /** 搜索开始时间 */
  333. searchBeginTime?: string;
  334. /** 搜索结束时间 */
  335. searchEndTime?: string;
  336. /** 排序字段 */
  337. sortField?: string;
  338. /** 排序方法,默认升序,否则降序(配合antd前端,约定参数为 Ascend,Dscend) */
  339. sortOrder?: string;
  340. /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
  341. descStr?: string;
  342. /** 复杂查询条件 */
  343. searchParameters?: Condition[];
  344. };
  345. /** 后端服务请求返回参数 */
  346. type ResponseType<T = any> = {
  347. /** 执行成功 */
  348. success?: boolean;
  349. /** 状态码 */
  350. code?: number;
  351. /** 错误码 */
  352. errorCode?: number;
  353. /** 错误信息 */
  354. errorMessage?: any;
  355. /** 消息显示类型 */
  356. showType?: number;
  357. /** 数据 */
  358. data?: T;
  359. /** 附加数据 */
  360. extras?: any;
  361. /** 时间戳 */
  362. timestamp?: number;
  363. };
  364. /** 分页数据对象 */
  365. type PageResponse<T = any> = {
  366. /** 当前页值 */
  367. pageIndex: number;
  368. /** 每页大小 */
  369. pageSize: number;
  370. /** 数据总数 */
  371. totalCount: number;
  372. /** 总页数 */
  373. totalPage?: number;
  374. /** 数据行 */
  375. items?: T[];
  376. };
  377. ${es}}
  378. }`,
  379. );
  380. execSync(`prettier --write ${TYPING_D}`);
  381. };
  382. /** 生成控制器文件 */
  383. const generateController = (paths, tagDict, enums) => {
  384. // 构造控制器
  385. let controllers = {};
  386. for (const apiPath in paths) {
  387. const actionName = getActionName(apiPath);
  388. const api = paths[apiPath];
  389. for (const verb in api) {
  390. // 只处理 post 和 get,其它忽略
  391. if (!['post', 'get'].includes(verb)) {
  392. continue;
  393. }
  394. const action = api[verb];
  395. const actionDesc = action.summary ?? actionName;
  396. // tag 作为控制器名称,如果没有 tag 则跳过
  397. const tags = action.tags ?? [];
  398. if (tags.length < 1) {
  399. continue;
  400. }
  401. for (const tag of tags) {
  402. const controllerName = getControllerName(tag);
  403. if (!controllers.hasOwnProperty(controllerName)) {
  404. controllers[controllerName] = {
  405. description: tagDict[tag],
  406. actions: [],
  407. actionNames: [],
  408. enums: [],
  409. };
  410. }
  411. let paramsType = '';
  412. let dataType = '';
  413. let responseType = '';
  414. if (action.parameters) {
  415. paramsType = getParamsType(action.parameters);
  416. }
  417. if (action.requestBody) {
  418. dataType = getRequestBodyType(action.requestBody);
  419. }
  420. if (action.responses) {
  421. responseType = getResponseType(action.responses);
  422. }
  423. const es = enums.filter(t => paramsType.indexOf(t) !== -1 || dataType.indexOf(t) !== -1 || responseType.indexOf(t) !== -1);
  424. if (es.length > 0) {
  425. controllers[controllerName].enums.push(es[0]);
  426. }
  427. const actionFullDesc = `/** ${actionDesc} ${verb.toUpperCase()} ${apiPath} */\n`;
  428. let actionBody = actionFullDesc;
  429. // export api
  430. if (actionName.toLowerCase().includes('export') || actionName.toLowerCase().includes('download')) {
  431. actionBody = `${actionFullDesc}export async function ${actionName}(`;
  432. if (verb == 'get') {
  433. if (paramsType != '') {
  434. actionBody = `${actionBody}params:${paramsType},`;
  435. }
  436. } else {
  437. if (paramsType != '') {
  438. actionBody = `${actionBody}params:${paramsType},`;
  439. }
  440. if (dataType != '') {
  441. const prefix = isSimpleType(dataType) ? '' : 'API.';
  442. actionBody = `${actionBody}data:${prefix}${dataType},`;
  443. }
  444. }
  445. actionBody = `${actionBody}options?:{[key:string]:any}){\n`;
  446. actionBody = `${actionBody}const url='${apiPath}';\n`;
  447. actionBody = `${actionBody}const config={method:'${verb.toUpperCase()}',`;
  448. if (verb == 'get' && paramsType != '') {
  449. actionBody = `${actionBody}params,`;
  450. }
  451. if (verb == 'post') {
  452. let pbv = '';
  453. if (paramsType != '') {
  454. pbv = 'params,';
  455. }
  456. if (dataType != '') {
  457. pbv = `${pbv}data,`;
  458. }
  459. actionBody = `${actionBody}${pbv}`;
  460. }
  461. actionBody = `${actionBody}...(options||{}), responseType: 'blob', getResponse: true} as RequestOptions;\n`;
  462. actionBody = `${actionBody}const res = await request(url, config);\n`;
  463. actionBody = `${actionBody}const hcd = res.request.getResponseHeader('Content-Disposition');\n`;
  464. actionBody = `${actionBody}if(!hcd){ return null; }\n`;
  465. actionBody = `${actionBody}let fileName = '';\n`;
  466. actionBody = `${actionBody}const cd = contentDisposition.parse(hcd)\n`;
  467. actionBody = `${actionBody}if (cd?.parameters?.filename) { fileName = cd?.parameters?.filename; }\n`;
  468. actionBody = `${actionBody}return { fileName, data:res.data };\n}\n`;
  469. }
  470. // other api
  471. else {
  472. actionBody = `${actionFullDesc}export async function ${actionName}(`;
  473. if (verb == 'get') {
  474. if (paramsType != '') {
  475. actionBody = `${actionBody}params:${paramsType},`;
  476. }
  477. } else {
  478. if (paramsType != '') {
  479. actionBody = `${actionBody}params:${paramsType},`;
  480. }
  481. if (dataType != '') {
  482. const prefix = isSimpleType(dataType) ? '' : 'API.';
  483. actionBody = `${actionBody}data:${prefix}${dataType},`;
  484. }
  485. }
  486. actionBody = `${actionBody}options?:{[key:string]:any}){\n`;
  487. actionBody = `${actionBody}const url='${apiPath}';\n`;
  488. actionBody = `${actionBody}const config={method:'${verb.toUpperCase()}',`;
  489. if (verb == 'get' && paramsType != '') {
  490. actionBody = `${actionBody}params,`;
  491. }
  492. if (verb == 'post') {
  493. let pbv = '';
  494. if (paramsType != '') {
  495. pbv = 'params,';
  496. }
  497. if (dataType != '') {
  498. pbv = `${pbv}data,`;
  499. }
  500. actionBody = `${actionBody}${pbv}`;
  501. }
  502. actionBody = `${actionBody}...(options||{})};\n`;
  503. actionBody = `${actionBody}const res = await request<API.ResponseType<${responseType}>>(url, config);`;
  504. actionBody = `${actionBody}return res?.data;\n}\n`;
  505. }
  506. controllers[controllerName].actions.push(actionBody);
  507. controllers[controllerName].actionNames.push(`${actionFullDesc}${actionName},\n`);
  508. }
  509. }
  510. }
  511. // 生成控制器文件
  512. let csnames = [];
  513. for (let ck in controllers) {
  514. const c = controllers[ck];
  515. const es = [...new Set(c.enums)].map((t) => `${t}, `);
  516. let importEnums = '';
  517. if (es.length > 0) {
  518. importEnums = `\nimport { ${es.join('')} } from '../enums';`;
  519. }
  520. csnames.push({ name: ck, description: c.description });
  521. const controllerFile = `${API_PATH}/${ck}.ts`;
  522. const isBolb = c.actionNames.findIndex(t => t.includes('export') || t.includes('download')) !== -1;
  523. fs.writeFileSync(
  524. controllerFile,
  525. `${UnifyComment}
  526. // --------------------------------------------------------------------------
  527. // ${c.description}
  528. // --------------------------------------------------------------------------
  529. import { request${isBolb ? ', RequestOptions' : ''} } from '@umijs/max';${importEnums}
  530. ${isBolb ? 'import contentDisposition from \'content-disposition\';' : ''}
  531. ${c.actions.join('\n')}
  532. export default {
  533. ${c.actionNames.join('')}
  534. };
  535. `,
  536. );
  537. execSync(`prettier --write ${controllerFile}`);
  538. }
  539. // 生成index.ts
  540. const ecs = csnames.map((t) => `import * as ${t.name} from './${t.name}';\n`);
  541. const edefaults = csnames.map((t) => `/** ${t.description} */\n${t.name},\n`);
  542. const indexFile = `${API_PATH}/index.ts`;
  543. fs.writeFileSync(
  544. indexFile,
  545. `${UnifyComment}
  546. ${ecs.join('')}
  547. export default {
  548. ${edefaults.join('')}
  549. };
  550. `,
  551. );
  552. execSync(`prettier --write ${indexFile}`);
  553. };
  554. // ----------------------------------------
  555. /** 生成接口服务 */
  556. const generate = (error, response, body) => {
  557. // OpenApi 描述文件
  558. const swagger = JSON.parse(body);
  559. // 实体模型
  560. const schemas = swagger['components']['schemas'];
  561. // API路径
  562. const apiPaths = swagger['paths'];
  563. // 开始处理
  564. console.info('生成后台接口文件');
  565. console.info(`-----------------\n${new Date().toLocaleString()}\n-----------------\n清理文件`);
  566. clearFiles(ROOT_PATH);
  567. if (!fs.existsSync(API_PATH)) {
  568. fs.mkdirSync(API_PATH);
  569. } else {
  570. clearFiles(API_PATH);
  571. }
  572. console.info('清理完成!');
  573. console.log('-----------------\n开始生成');
  574. // 生成枚举文件
  575. console.log('> 生成枚举文件');
  576. const enums = generateEnums(schemas);
  577. // 生成实体类型文件
  578. console.log('> 生成实体类型文件');
  579. generateEntity(schemas, enums);
  580. // 生成控制器文件
  581. console.log('> 生成控制器文件');
  582. generateController(apiPaths, getTagDescDict(swagger['tags'] ?? []), enums);
  583. console.info('生成完成!');
  584. };
  585. const request = require('request');
  586. request.get(config.swaggerJson, generate);