This commit is contained in:
2024-03-01 20:28:14 +08:00
commit 076c21dc36
491 changed files with 84482 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
import swaggerAutoSync from './swaggerAutoSync/swaggerAutoSync.js'
function hander(routers) {
routers.test = {
name: 'Swagger自动同步',
component: swaggerAutoSync
};
}
module.exports = function() {
this.bindHook('sub_setting_nav', hander);
};

View File

@@ -0,0 +1,62 @@
const baseController = require('controllers/base.js');
const yapi = require('yapi.js');
const syncModel = require('../syncModel.js');
const projectModel = require('models/project.js');
const interfaceSyncUtils = require('../interfaceSyncUtils.js')
class syncController extends baseController {
constructor(ctx) {
super(ctx);
this.syncModel = yapi.getInst(syncModel);
this.projectModel = yapi.getInst(projectModel);
this.interfaceSyncUtils = yapi.getInst(interfaceSyncUtils);
}
/**
* 保存定时任务
* @param {*} ctx
*/
async upSync(ctx) {
let requestBody = ctx.request.body;
let projectId = requestBody.project_id;
if (!projectId) {
return (ctx.body = yapi.commons.resReturn(null, 408, '缺少项目Id'));
}
if ((await this.checkAuth(projectId, 'project', 'edit')) !== true) {
return (ctx.body = yapi.commons.resReturn(null, 405, '没有权限'));
}
let result;
if (requestBody.id) {
result = await this.syncModel.up(requestBody);
} else {
result = await this.syncModel.save(requestBody);
}
//操作定时任务
if (requestBody.is_sync_open) {
this.interfaceSyncUtils.addSyncJob(projectId, requestBody.sync_cron, requestBody.sync_json_url, requestBody.sync_mode, requestBody.uid);
} else {
this.interfaceSyncUtils.deleteSyncJob(projectId);
}
return (ctx.body = yapi.commons.resReturn(result));
}
/**
* 查询定时任务
* @param {*} ctx
*/
async getSync(ctx) {
let projectId = ctx.query.project_id;
if (!projectId) {
return (ctx.body = yapi.commons.resReturn(null, 408, '缺少项目Id'));
}
let result = await this.syncModel.getByProjectId(projectId);
return (ctx.body = yapi.commons.resReturn(result));
}
}
module.exports = syncController;

View File

@@ -0,0 +1,4 @@
module.exports = {
server: true,
client: true
}

View File

@@ -0,0 +1,221 @@
const schedule = require('node-schedule');
const openController = require('controllers/open.js');
const projectModel = require('models/project.js');
const syncModel = require('./syncModel.js');
const tokenModel = require('models/token.js');
const yapi = require('yapi.js')
const sha = require('sha.js');
const md5 = require('md5');
const { getToken } = require('utils/token');
const jobMap = new Map();
class syncUtils {
constructor(ctx) {
yapi.commons.log("-------------------------------------swaggerSyncUtils constructor-----------------------------------------------");
this.ctx = ctx;
this.openController = yapi.getInst(openController);
this.syncModel = yapi.getInst(syncModel);
this.tokenModel = yapi.getInst(tokenModel)
this.projectModel = yapi.getInst(projectModel);
this.init()
}
//初始化定时任务
async init() {
let allSyncJob = await this.syncModel.listAll();
for (let i = 0, len = allSyncJob.length; i < len; i++) {
let syncItem = allSyncJob[i];
if (syncItem.is_sync_open) {
this.addSyncJob(syncItem.project_id, syncItem.sync_cron, syncItem.sync_json_url, syncItem.sync_mode, syncItem.uid);
}
}
}
/**
* 新增同步任务.
* @param {*} projectId 项目id
* @param {*} cronExpression cron表达式,针对定时任务
* @param {*} swaggerUrl 获取swagger的地址
* @param {*} syncMode 同步模式
* @param {*} uid 用户id
*/
async addSyncJob(projectId, cronExpression, swaggerUrl, syncMode, uid) {
if(!swaggerUrl)return;
let projectToken = await this.getProjectToken(projectId, uid);
//立即执行一次
this.syncInterface(projectId, swaggerUrl, syncMode, uid, projectToken);
let scheduleItem = schedule.scheduleJob(cronExpression, async () => {
this.syncInterface(projectId, swaggerUrl, syncMode, uid, projectToken);
});
//判断是否已经存在这个任务
let jobItem = jobMap.get(projectId);
if (jobItem) {
jobItem.cancel();
}
jobMap.set(projectId, scheduleItem);
}
//同步接口
async syncInterface(projectId, swaggerUrl, syncMode, uid, projectToken) {
yapi.commons.log('定时器触发, syncJsonUrl:' + swaggerUrl + ",合并模式:" + syncMode);
let oldPorjectData;
try {
oldPorjectData = await this.projectModel.get(projectId);
} catch(e) {
yapi.commons.log('获取项目:' + projectId + '失败');
this.deleteSyncJob(projectId);
//删除数据库定时任务
await this.syncModel.delByProjectId(projectId);
return;
}
//如果项目已经删除了
if (!oldPorjectData) {
yapi.commons.log('项目:' + projectId + '不存在');
this.deleteSyncJob(projectId);
//删除数据库定时任务
await this.syncModel.delByProjectId(projectId);
return;
}
let newSwaggerJsonData;
try {
newSwaggerJsonData = await this.getSwaggerContent(swaggerUrl)
if (!newSwaggerJsonData || typeof newSwaggerJsonData !== 'object') {
yapi.commons.log('数据格式出错,请检查')
this.saveSyncLog(0, syncMode, "数据格式出错,请检查", uid, projectId);
}
newSwaggerJsonData = JSON.stringify(newSwaggerJsonData)
} catch (e) {
this.saveSyncLog(0, syncMode, "获取数据失败,请检查", uid, projectId);
yapi.commons.log('获取数据失败' + e.message)
}
let oldSyncJob = await this.syncModel.getByProjectId(projectId);
//更新之前判断本次swagger json数据是否跟上次的相同,相同则不更新
if (newSwaggerJsonData && oldSyncJob.old_swagger_content && oldSyncJob.old_swagger_content == md5(newSwaggerJsonData)) {
//记录日志
// this.saveSyncLog(0, syncMode, "接口无更新", uid, projectId);
oldSyncJob.last_sync_time = yapi.commons.time();
await this.syncModel.upById(oldSyncJob._id, oldSyncJob);
return;
}
let _params = {
type: 'swagger',
json: newSwaggerJsonData,
project_id: projectId,
merge: syncMode,
token: projectToken
}
let requestObj = {
params: _params
};
await this.openController.importData(requestObj);
//同步成功就更新同步表的数据
if (requestObj.body.errcode == 0) {
//修改sync_model的属性
oldSyncJob.last_sync_time = yapi.commons.time();
oldSyncJob.old_swagger_content = md5(newSwaggerJsonData);
await this.syncModel.upById(oldSyncJob._id, oldSyncJob);
}
//记录日志
this.saveSyncLog(requestObj.body.errcode, syncMode, requestObj.body.errmsg, uid, projectId);
}
getSyncJob(projectId) {
return jobMap.get(projectId);
}
deleteSyncJob(projectId) {
let jobItem = jobMap.get(projectId);
if (jobItem) {
jobItem.cancel();
}
}
/**
* 记录同步日志
* @param {*} errcode
* @param {*} syncMode
* @param {*} moremsg
* @param {*} uid
* @param {*} projectId
*/
saveSyncLog(errcode, syncMode, moremsg, uid, projectId) {
yapi.commons.saveLog({
content: '自动同步接口状态:' + (errcode == 0 ? '成功,' : '失败,') + "合并模式:" + this.getSyncModeName(syncMode) + ",更多信息:" + moremsg,
type: 'project',
uid: uid,
username: "自动同步用户",
typeid: projectId
});
}
/**
* 获取项目token,因为导入接口需要鉴权.
* @param {*} project_id 项目id
* @param {*} uid 用户id
*/
async getProjectToken(project_id, uid) {
try {
let data = await this.tokenModel.get(project_id);
let token;
if (!data) {
let passsalt = yapi.commons.randStr();
token = sha('sha1')
.update(passsalt)
.digest('hex')
.substr(0, 20);
await this.tokenModel.save({ project_id, token });
} else {
token = data.token;
}
token = getToken(token, uid);
return token;
} catch (err) {
return "";
}
}
getUid(uid) {
return parseInt(uid, 10);
}
/**
* 转换合并模式的值为中文.
* @param {*} syncMode 合并模式
*/
getSyncModeName(syncMode) {
if (syncMode == 'good') {
return '智能合并';
} else if (syncMode == 'normal') {
return '普通模式';
} else if (syncMode == 'merge') {
return '完全覆盖';
}
return '';
}
async getSwaggerContent(swaggerUrl) {
const axios = require('axios')
try {
let response = await axios.get(swaggerUrl);
if (response.status > 400) {
throw new Error(`http status "${response.status}"` + '获取数据失败,请确认 swaggerUrl 是否正确')
}
return response.data;
} catch (e) {
let response = e.response || {status: e.message || 'error'};
throw new Error(`http status "${response.status}"` + '获取数据失败,请确认 swaggerUrl 是否正确')
}
}
}
module.exports = syncUtils;

View File

@@ -0,0 +1,23 @@
const controller = require('./controller/syncController.js');
const yapi =require('yapi.js');
const interfaceSyncUtils = require('./interfaceSyncUtils.js');
module.exports = function () {
yapi.getInst(interfaceSyncUtils);
this.bindHook('add_router', function (addRouter) {
addRouter({
controller: controller,
method: 'get',
path: 'autoSync/get',
action: 'getSync'
});
addRouter({
controller: controller,
method: 'post',
path: 'autoSync/save',
action: 'upSync'
});
});
};

View File

@@ -0,0 +1,239 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { formatTime } from 'client/common.js';
import { Form, Switch, Button, Icon, Tooltip, message, Input, Select } from 'antd';
import {handleSwaggerUrlData} from 'client/reducer/modules/project';
const FormItem = Form.Item;
const Option = Select.Option;
import axios from 'axios';
// layout
const formItemLayout = {
labelCol: {
lg: { span: 5 },
xs: { span: 24 },
sm: { span: 10 }
},
wrapperCol: {
lg: { span: 16 },
xs: { span: 24 },
sm: { span: 12 }
},
className: 'form-item'
};
const tailFormItemLayout = {
wrapperCol: {
sm: {
span: 16,
offset: 11
}
}
};
@connect(
state => {
return {
projectMsg: state.project.currProject
};
},
{
handleSwaggerUrlData
}
)
@Form.create()
export default class ProjectInterfaceSync extends Component {
static propTypes = {
form: PropTypes.object,
match: PropTypes.object,
projectId: PropTypes.number,
projectMsg: PropTypes.object,
handleSwaggerUrlData: PropTypes.func
};
constructor(props) {
super(props);
this.state = {
sync_data: { is_sync_open: false }
};
}
handleSubmit = async () => {
const { form, projectId } = this.props;
let params = {
project_id: projectId,
is_sync_open: this.state.sync_data.is_sync_open,
uid: this.props.projectMsg.uid
};
if (this.state.sync_data._id) {
params.id = this.state.sync_data._id;
}
form.validateFields(async (err, values) => {
if (!err) {
let assignValue = Object.assign(params, values);
await axios.post('/api/plugin/autoSync/save', assignValue).then(res => {
if (res.data.errcode === 0) {
message.success('保存成功');
} else {
message.error(res.data.errmsg);
}
});
}
});
};
validSwaggerUrl = async (rule, value, callback) => {
if(!value)return;
try{
await this.props.handleSwaggerUrlData(value);
} catch(e) {
callback('swagger地址不正确');
}
callback()
}
UNSAFE_componentWillMount() {
//查询同步任务
this.setState({
sync_data: {}
});
//默认每份钟同步一次,取一个随机数
this.setState({
random_corn: '*/2 * * * *'
});
this.getSyncData();
}
async getSyncData() {
let projectId = this.props.projectMsg._id;
let result = await axios.get('/api/plugin/autoSync/get?project_id=' + projectId);
if (result.data.errcode === 0) {
if (result.data.data) {
this.setState({
sync_data: result.data.data
});
}
}
}
// 是否开启
onChange = v => {
let sync_data = this.state.sync_data;
sync_data.is_sync_open = v;
this.setState({
sync_data: sync_data
});
};
sync_cronCheck(rule, value, callback){
if(!value)return;
value = value.trim();
if(value.split(/ +/).length > 5){
callback('不支持秒级别的设置,建议使用 "*/10 * * * *" ,每隔10分钟更新')
}
callback()
}
render() {
const { getFieldDecorator } = this.props.form;
return (
<div className="m-panel">
<Form>
<FormItem
label="是否开启自动同步"
{...formItemLayout}
>
<Switch
checked={this.state.sync_data.is_sync_open}
onChange={this.onChange}
checkedChildren="开"
unCheckedChildren="关"
/>
{this.state.sync_data.last_sync_time != null ? (<div>上次更新时间:<span className="logtime">{formatTime(this.state.sync_data.last_sync_time)}</span></div>) : null}
</FormItem>
<div>
<FormItem {...formItemLayout} label={
<span className="label">
数据同步&nbsp;
<Tooltip
title={
<div>
<h3 style={{ color: 'white' }}>普通模式</h3>
<p>不导入已存在的接口</p>
<br />
<h3 style={{ color: 'white' }}>智能合并</h3>
<p>
已存在的接口将合并返回数据的 response适用于导入了 swagger
数据保留对数据结构的改动
</p>
<br />
<h3 style={{ color: 'white' }}>完全覆盖</h3>
<p>不保留旧数据完全使用新数据适用于接口定义完全交给后端定义</p>
</div>
}
>
<Icon type="question-circle-o" />
</Tooltip>{' '}
</span>
}>
{getFieldDecorator('sync_mode', {
initialValue: this.state.sync_data.sync_mode,
rules: [
{
required: true,
message: '请选择同步方式!'
}
]
})(
<Select>
<Option value="normal">普通模式</Option>
<Option value="good">智能合并</Option>
<Option value="merge">完全覆盖</Option>
</Select>
)}
</FormItem>
<FormItem {...formItemLayout} label="项目的swagger json地址">
{getFieldDecorator('sync_json_url', {
rules: [
{
required: true,
message: '输入swagger地址'
},
{
validator: this.validSwaggerUrl
}
],
validateTrigger: 'onBlur',
initialValue: this.state.sync_data.sync_json_url
})(<Input />)}
</FormItem>
<FormItem {...formItemLayout} label={<span>类cron风格表达式(默认10分钟更新一次)&nbsp;<a href="https://blog.csdn.net/shouldnotappearcalm/article/details/89469047">参考</a></span>}>
{getFieldDecorator('sync_cron', {
rules: [
{
required: true,
message: '输入node-schedule的类cron表达式!'
},
{
validator: this.sync_cronCheck
}
],
initialValue: this.state.sync_data.sync_cron ? this.state.sync_data.sync_cron : this.state.random_corn
})(<Input />)}
</FormItem>
</div>
<FormItem {...tailFormItemLayout}>
<Button type="primary" htmlType="submit" icon="save" size="large" onClick={this.handleSubmit}>
保存
</Button>
</FormItem>
</Form>
</div>
);
}
}

View File

@@ -0,0 +1,90 @@
const yapi = require('yapi.js');
const baseModel = require('models/base.js');
const mongoose = require('mongoose');
class syncModel extends baseModel {
getName() {
return 'interface_auto_sync';
}
getSchema() {
return {
uid: { type: Number},
project_id: { type: Number, required: true },
//是否开启自动同步
is_sync_open: { type: Boolean, default: false },
//自动同步定时任务的cron表达式
sync_cron: String,
//自动同步获取json的url
sync_json_url: String,
//接口合并模式 good,nomarl等等 意思也就是智能合并,完全覆盖等
sync_mode: String,
//上次成功同步接口时间,
last_sync_time: Number,
//上次同步的swagger 文档内容
old_swagger_content: String,
add_time: Number,
up_time: Number,
};
}
getByProjectId(id) {
return this.model.findOne({
project_id: id
})
}
delByProjectId(project_id){
return this.model.remove({
project_id: project_id
})
}
save(data) {
data.up_time = yapi.commons.time();
let m = new this.model(data);
return m.save();
}
listAll() {
return this.model
.find({})
.select(
'_id uid project_id add_time up_time is_sync_open sync_cron sync_json_url sync_mode old_swagger_content last_sync_time'
)
.sort({ _id: -1 })
.exec();
}
up(data) {
let id = data.id;
delete data.id;
data.up_time = yapi.commons.time();
return this.model.update({
_id: id
}, data)
}
upById(id, data) {
delete data.id;
data.up_time = yapi.commons.time();
return this.model.update({
_id: id
}, data)
}
del(id){
return this.model.remove({
_id: id
})
}
delByProjectId(projectId){
return this.model.remove({
project_id: projectId
})
}
}
module.exports = syncModel;