123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652 |
- /**
- * 根据 Swagger json 自动生成接口服务文件
- */
- const config = require('./config');
- // 输出文件及目录定义
- const ROOT_PATH = './src/services';
- const ENUM_FILE = `${ROOT_PATH}/enums.ts`;
- const TYPING_D = `${ROOT_PATH}/typing.d.ts`;
- const API_PATH = `${ROOT_PATH}/apis`;
- // JS关键字
- const JS_KEY_WORDS = [
- 'break', 'else', 'new', 'var', 'case', 'finally', 'return', 'void', 'catch', 'for', 'switch',
- 'while', 'continue', 'function', 'this', 'with', 'default', 'if', 'throw', 'delete', 'in',
- 'try', 'do', 'instranceof', 'typeof', 'abstract', 'enum', 'int', 'short', 'boolean', 'export',
- 'interface', 'static', 'byte', 'extends', 'long', 'super', 'char', 'final', 'native', 'synchronized',
- 'class', 'float', 'package', 'throws', 'const', 'goto', 'private ', 'transient', 'debugger',
- 'implements', 'protected ', 'volatile', 'double', 'import', 'public',
- ];
- /** 数值类型映射 */
- const IntegerMapping = {
- int16: 'number',
- int32: 'number',
- int64: 'string',
- };
- /** 返回结果类型映射 */
- const ResultMapping = {
- Int16: 'number',
- Int32: 'number',
- Int64: 'string',
- Boolean: 'boolean',
- Object: 'any',
- String: 'string',
- DateTime: 'string',
- };
- /** 统一注释 */
- const UnifyComment = `// @ts-ignore\n/* eslint-disable */\n\n// 该文件自动生成,请勿手动修改!`;
- // ----------------------------------------
- // 执行外部命令
- const execSync = require('child_process').execSync;
- // 文件操作
- const fs = require('fs');
- // ----------------------------------------
- /**
- * 下划线中横线分隔字符串转换为驼峰格式
- * @param {string} value 待转换字符串
- * @param {boolean} [capitalize] 首字母大写
- * @returns
- */
- const kebabToCamelCase = (value, capitalize = false) => {
- let n = value.replace(/[_-][a-zA-z]/g, (str) => str.substr(-1).toUpperCase()).trim() || '';
- if (capitalize && n.length != 0) {
- n = n.slice(0, 1).toUpperCase() + n.slice(1);
- }
- return n;
- };
- /**
- * 根据路径生成操作名称
- * @param {string} path 接口路径
- * @returns 取路径最后一级的名称
- */
- const getActionName = (path) => {
- const pp = path.split('/');
- if (pp.length == 0) {
- return '';
- }
- const n = pp[pp.length - 1];
- let an = kebabToCamelCase(n);
- if (JS_KEY_WORDS.includes(an.toLocaleLowerCase())) {
- an = `${an}Action`;
- }
- return an;
- };
- /**
- * 生成控制器名称
- * @param {string} name Swagger的tag名称
- * @returns
- */
- const getControllerName = (name) => {
- const n = kebabToCamelCase(name, true);
- return `${n}Controller`;
- };
- /**
- * 清除文件
- * @param {*} dir 需要清空的目录
- */
- const clearFiles = (dir) => {
- const oldFiles = fs.readdirSync(dir);
- oldFiles.forEach((f) => {
- const filePath = `${dir}/${f}`;
- const s = fs.statSync(filePath);
- if (s.isDirectory()) {
- return;
- }
- fs.unlinkSync(filePath);
- });
- };
- /** 获取引用类型 */
- const getRefEntity = ($ref) => {
- const rs = $ref.split('/');
- if (rs.length == 0) {
- return 'any';
- }
- return rs[rs.length - 1];
- };
- /** 获取简单类型 */
- const getSimpleType = ({ type, format }) => {
- if (!type) {
- return 'any';
- }
- const ltype = type.toLowerCase();
- if (['string', 'boolean', 'number'].includes(type)) {
- return ltype;
- }
- if (ltype == 'integer') {
- return IntegerMapping[format] ?? 'number';
- }
- return 'any';
- };
- /** 获取响应结果类型 */
- const getResultType = (type) => {
- const t = ResultMapping[type];
- if (t) {
- return t;
- }
- return `API.${type}`;
- };
- /** 获取实体类型 */
- const getEntityType = (name, schema) => {
- const props = schema.properties;
- let entity = '';
- const desc = schema.description;
- if (desc) {
- entity = `/** ${desc} */\n`;
- }
- entity = `${entity}type ${name} = {\n`;
- const requiredList = schema.required ?? [];
- for (const pname in props) {
- const prop = props[pname];
- let member = '';
- const memberDesc = prop.description;
- if (memberDesc) {
- member = `/** ${memberDesc} */\n`;
- }
- let memberType = '';
- if (prop['$ref']) {
- memberType = `${getRefEntity(prop['$ref'])}`;
- } else if (prop['items']) {
- if (prop['items']['$ref']) {
- memberType = `${getRefEntity(prop['items']['$ref'])}[]`;
- } else {
- memberType = `${getSimpleType(prop['items'])}[]`;
- }
- } else {
- memberType = `${getSimpleType(prop)}`;
- }
- const required = requiredList.includes(pname) || (prop.nullable !== undefined && !prop.nullable);
- member = `${member}${pname}${required ? '' : '?'}: ${memberType};\n`;
- entity = `${entity}${member}`;
- }
- entity = `${entity}}\n`;
- return entity;
- };
- /** 获取请求类型 */
- const getRequestBodyType = (reqBody) => {
- if (reqBody['content']['application/json']) {
- const schema = reqBody['content']['application/json']['schema'];
- let tps = '';
- if (schema['$ref']) {
- tps = getRefEntity(schema['$ref']);
- } else if (schema['items']) {
- if (schema['items']['$ref']) {
- tps = getRefEntity(schema['items']['$ref']);
- } else {
- tps = getSimpleType(schema['items']);
- }
- } else {
- tps = getSimpleType(schema);
- }
- if (schema['type'] == 'array') {
- return `${tps}[]`;
- }
- return tps;
- }
- if (reqBody['content']['multipart/form-data']) {
- return 'FormData';
- }
- return 'any';
- };
- /** 获取统一返回类型 */
- const getXnRestfulResultType = (propRef) => {
- const rtype = getRefEntity(propRef).replaceAll('XnRestfulResult_', '');
- const ps = rtype.split('_');
- if (ps.length == 1) {
- return getResultType(ps);
- }
- if (ps[0] == 'List') {
- return `${getResultType(rtype.replaceAll('List_', ''))}[]`;
- }
- if (ps[0] == 'PageResult') {
- return `API.PageResponse<${getResultType(rtype.replaceAll('PageResult_', ''))}>`;
- }
- return getResultType(rtype);
- };
- /** 获取响应类型 */
- const getResponseType = (res) => {
- const schema = res['200']?.['content']?.['application/json']?.['schema'];
- if (!schema) {
- return 'any';
- }
- if (schema['$ref']) {
- return getXnRestfulResultType(schema['$ref']);
- } else if (schema['items']) {
- if (schema['items']['$ref']) {
- return getXnRestfulResultType(schema['items']['$ref']);
- } else {
- return getSimpleType(schema['items']);
- }
- } else {
- return getSimpleType(schema);
- }
- };
- /** 获取查询参数类型 */
- const getParamsType = (parameters) => {
- let pb = '';
- for (const p of parameters) {
- const pdesc = p.description ?? p.name;
- pb = `${pb}/** ${pdesc} */\n${p.name}${p.required ? '' : '?'}:`;
- const prop = p.schema;
- let ptype = '';
- if (prop['$ref']) {
- ptype = `${getRefEntity(prop['$ref'])}`;
- } else if (prop['items']) {
- if (prop['items']['$ref']) {
- ptype = `${getRefEntity(prop['items']['$ref'])}[]`;
- } else {
- ptype = `${getSimpleType(prop['items'])}[]`;
- }
- } else {
- ptype = `${getSimpleType(prop)}`;
- }
- pb = `${pb}${ptype};\n`;
- }
- return `{\n${pb}}`;
- };
- /** 判断是否简单类型 */
- const isSimpleType = (type) => {
- return ['string', 'number', 'boolean', 'object', 'any', 'formdata'].includes(type.toLowerCase());
- };
- /** 获取 tag 描述字典 */
- const getTagDescDict = (tags) => {
- let tagDict = {};
- for (const tag of tags) {
- tagDict[tag.name] = tag.description ?? tag.name;
- }
- return tagDict;
- };
- // ----------------------------------------
- /** 生成枚举 */
- const generateEnums = (schemas) => {
- const enums = [];
- const importEnums = [];
- for (let ek in schemas) {
- if (schemas[ek].enum) {
- const desc = schemas[ek].description;
- if (!desc) {
- continue;
- }
- const s1 = desc.replaceAll('<br />', '').split(' ');
- if (s1 && s1.length > 1) {
- const enumComment = s1?.[0];
- const items = [];
- for (let j = 1; j < s1.length; j++) {
- const s2 = s1[j].split(' ');
- if (s2.length > 3) {
- const itemComent = s2[0];
- const itemCode = s2[1];
- const itemValue = s2[3];
- items.push(`\n /** ${itemComent} */\n ${itemCode} = ${itemValue},`);
- }
- }
- enums.push(`\n/** ${enumComment} */\nexport enum ${ek} {${items.join('')}\n}`);
- importEnums.push(ek);
- }
- }
- }
- const fs = require('fs');
- const es = enums.join('\n');
- const ies = importEnums.map((t) => ` ${t}`).join(',\n');
- fs.writeFileSync(ENUM_FILE, `${UnifyComment}\n\n${es}\n\nexport default {\n${ies},\n};\n`);
- execSync(`prettier --write ${ENUM_FILE}`);
- return importEnums;
- };
- /** 生成类型定义文件 */
- const generateEntity = (schemas, enums) => {
- // 生成类型
- let entities = [];
- for (const name in schemas) {
- const schema = schemas[name];
- if (
- name.includes('XnRestfulResult_') ||
- name.includes('PageResult_') ||
- schema.enum ||
- !schema.properties
- ) {
- continue;
- }
- // const entity = getEntityType(name.split('_')[0], schema);
- // const ns = name.split('_');
- // const entity = getEntityType(ns[ns.length - 1], schema);
- const entity = getEntityType(name, schema);
- entities.push(entity);
- }
- const fs = require('fs');
- const ies = enums.map((t) => ` ${t}`).join(',\n');
- const es = entities.join('\n');
- fs.writeFileSync(
- TYPING_D,
- `${UnifyComment}
- import {
- ${ies},
- } from './enums';
- declare global {
- declare namespace API {
- /** 分页查询请求参数基类型 */
- type PageQueryType = {
- /** 当前页值 */
- pageIndex: number;
- /** 每页大小 */
- pageSize: number;
- /** 搜索值 */
- searchValue?: string;
- /** 搜索开始时间 */
- searchBeginTime?: string;
- /** 搜索结束时间 */
- searchEndTime?: string;
- /** 排序字段 */
- sortField?: string;
- /** 排序方法,默认升序,否则降序(配合antd前端,约定参数为 Ascend,Dscend) */
- sortOrder?: string;
- /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
- descStr?: string;
- /** 复杂查询条件 */
- searchParameters?: Condition[];
- };
- /** 后端服务请求返回参数 */
- type ResponseType<T = any> = {
- /** 执行成功 */
- success?: boolean;
- /** 状态码 */
- code?: number;
- /** 错误码 */
- errorCode?: number;
- /** 错误信息 */
- errorMessage?: any;
- /** 消息显示类型 */
- showType?: number;
- /** 数据 */
- data?: T;
- /** 附加数据 */
- extras?: any;
- /** 时间戳 */
- timestamp?: number;
- };
- /** 分页数据对象 */
- type PageResponse<T = any> = {
- /** 当前页值 */
- pageIndex: number;
- /** 每页大小 */
- pageSize: number;
- /** 数据总数 */
- totalCount: number;
- /** 总页数 */
- totalPage?: number;
- /** 数据行 */
- items?: T[];
- };
- ${es}}
- }`,
- );
- execSync(`prettier --write ${TYPING_D}`);
- };
- /** 生成控制器文件 */
- const generateController = (paths, tagDict, enums) => {
- // 构造控制器
- let controllers = {};
- for (const apiPath in paths) {
- const actionName = getActionName(apiPath);
- const api = paths[apiPath];
- for (const verb in api) {
- // 只处理 post 和 get,其它忽略
- if (!['post', 'get'].includes(verb)) {
- continue;
- }
- const action = api[verb];
- const actionDesc = action.summary ?? actionName;
- // tag 作为控制器名称,如果没有 tag 则跳过
- const tags = action.tags ?? [];
- if (tags.length < 1) {
- continue;
- }
- for (const tag of tags) {
- const controllerName = getControllerName(tag);
- if (!controllers.hasOwnProperty(controllerName)) {
- controllers[controllerName] = {
- description: tagDict[tag],
- actions: [],
- actionNames: [],
- enums: [],
- };
- }
- let paramsType = '';
- let dataType = '';
- let responseType = '';
- if (action.parameters) {
- paramsType = getParamsType(action.parameters);
- }
- if (action.requestBody) {
- dataType = getRequestBodyType(action.requestBody);
- }
- if (action.responses) {
- responseType = getResponseType(action.responses);
- }
- const es = enums.filter(t => paramsType.indexOf(t) !== -1 || dataType.indexOf(t) !== -1 || responseType.indexOf(t) !== -1);
- if (es.length > 0) {
- controllers[controllerName].enums.push(es[0]);
- }
- const actionFullDesc = `/** ${actionDesc} ${verb.toUpperCase()} ${apiPath} */\n`;
- let actionBody = actionFullDesc;
- // export api
- if (actionName.toLowerCase().includes('export') || actionName.toLowerCase().includes('download')) {
- actionBody = `${actionFullDesc}export async function ${actionName}(`;
- if (verb == 'get') {
- if (paramsType != '') {
- actionBody = `${actionBody}params:${paramsType},`;
- }
- } else {
- if (paramsType != '') {
- actionBody = `${actionBody}params:${paramsType},`;
- }
- if (dataType != '') {
- const prefix = isSimpleType(dataType) ? '' : 'API.';
- actionBody = `${actionBody}data:${prefix}${dataType},`;
- }
- }
- actionBody = `${actionBody}options?:{[key:string]:any}){\n`;
- actionBody = `${actionBody}const url='${apiPath}';\n`;
- actionBody = `${actionBody}const config={method:'${verb.toUpperCase()}',`;
- if (verb == 'get' && paramsType != '') {
- actionBody = `${actionBody}params,`;
- }
- if (verb == 'post') {
- let pbv = '';
- if (paramsType != '') {
- pbv = 'params,';
- }
- if (dataType != '') {
- pbv = `${pbv}data,`;
- }
- actionBody = `${actionBody}${pbv}`;
- }
- actionBody = `${actionBody}...(options||{}), responseType: 'blob', getResponse: true} as RequestOptions;\n`;
- actionBody = `${actionBody}const res = await request(url, config);\n`;
- actionBody = `${actionBody}const hcd = res.request.getResponseHeader('Content-Disposition');\n`;
- actionBody = `${actionBody}if(!hcd){ return null; }\n`;
- actionBody = `${actionBody}let fileName = '';\n`;
- actionBody = `${actionBody}const cd = contentDisposition.parse(hcd)\n`;
- actionBody = `${actionBody}if (cd?.parameters?.filename) { fileName = cd?.parameters?.filename; }\n`;
- actionBody = `${actionBody}return { fileName, data:res.data };\n}\n`;
- }
- // other api
- else {
- actionBody = `${actionFullDesc}export async function ${actionName}(`;
- if (verb == 'get') {
- if (paramsType != '') {
- actionBody = `${actionBody}params:${paramsType},`;
- }
- } else {
- if (paramsType != '') {
- actionBody = `${actionBody}params:${paramsType},`;
- }
- if (dataType != '') {
- const prefix = isSimpleType(dataType) ? '' : 'API.';
- actionBody = `${actionBody}data:${prefix}${dataType},`;
- }
- }
- actionBody = `${actionBody}options?:{[key:string]:any}){\n`;
- actionBody = `${actionBody}const url='${apiPath}';\n`;
- actionBody = `${actionBody}const config={method:'${verb.toUpperCase()}',`;
- if (verb == 'get' && paramsType != '') {
- actionBody = `${actionBody}params,`;
- }
- if (verb == 'post') {
- let pbv = '';
- if (paramsType != '') {
- pbv = 'params,';
- }
- if (dataType != '') {
- pbv = `${pbv}data,`;
- }
- actionBody = `${actionBody}${pbv}`;
- }
- actionBody = `${actionBody}...(options||{})};\n`;
- actionBody = `${actionBody}const res = await request<API.ResponseType<${responseType}>>(url, config);`;
- actionBody = `${actionBody}return res?.data;\n}\n`;
- }
- controllers[controllerName].actions.push(actionBody);
- controllers[controllerName].actionNames.push(`${actionFullDesc}${actionName},\n`);
- }
- }
- }
- // 生成控制器文件
- let csnames = [];
- for (let ck in controllers) {
- const c = controllers[ck];
- const es = [...new Set(c.enums)].map((t) => `${t}, `);
- let importEnums = '';
- if (es.length > 0) {
- importEnums = `\nimport { ${es.join('')} } from '../enums';`;
- }
- csnames.push({ name: ck, description: c.description });
- const controllerFile = `${API_PATH}/${ck}.ts`;
- const isBolb = c.actionNames.findIndex(t => t.includes('export') || t.includes('download')) !== -1;
- fs.writeFileSync(
- controllerFile,
- `${UnifyComment}
- // --------------------------------------------------------------------------
- // ${c.description}
- // --------------------------------------------------------------------------
- import { request${isBolb ? ', RequestOptions' : ''} } from '@umijs/max';${importEnums}
- ${isBolb ? 'import contentDisposition from \'content-disposition\';' : ''}
- ${c.actions.join('\n')}
-
- export default {
- ${c.actionNames.join('')}
- };
- `,
- );
- execSync(`prettier --write ${controllerFile}`);
- }
- // 生成index.ts
- const ecs = csnames.map((t) => `import * as ${t.name} from './${t.name}';\n`);
- const edefaults = csnames.map((t) => `/** ${t.description} */\n${t.name},\n`);
- const indexFile = `${API_PATH}/index.ts`;
- fs.writeFileSync(
- indexFile,
- `${UnifyComment}
- ${ecs.join('')}
- export default {
- ${edefaults.join('')}
- };
- `,
- );
- execSync(`prettier --write ${indexFile}`);
- };
- // ----------------------------------------
- /** 生成接口服务 */
- const generate = (error, response, body) => {
- // OpenApi 描述文件
- const swagger = JSON.parse(body);
- // 实体模型
- const schemas = swagger['components']['schemas'];
- // API路径
- const apiPaths = swagger['paths'];
- // 开始处理
- console.info('生成后台接口文件');
- console.info(`-----------------\n${new Date().toLocaleString()}\n-----------------\n清理文件`);
- clearFiles(ROOT_PATH);
- if (!fs.existsSync(API_PATH)) {
- fs.mkdirSync(API_PATH);
- } else {
- clearFiles(API_PATH);
- }
- console.info('清理完成!');
- console.log('-----------------\n开始生成');
- // 生成枚举文件
- console.log('> 生成枚举文件');
- const enums = generateEnums(schemas);
- // 生成实体类型文件
- console.log('> 生成实体类型文件');
- generateEntity(schemas, enums);
- // 生成控制器文件
- console.log('> 生成控制器文件');
- generateController(apiPaths, getTagDescDict(swagger['tags'] ?? []), enums);
- console.info('生成完成!');
- };
- const request = require('request');
- request.get(config.swaggerJson, generate);
|