index.js 21 KB

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