如何查看eas源代码_MT5CTP扩展:MT4源代码(EA)适配器来了
MT5CTP項目受到大家的極大關注和積極測試、應用,始料未及。朋友們都反映積累了海量的mt4源代碼,做mt5的代碼轉換都有點吃力,更不用說再修改到適配MT5CTP項目來直接驅動國內期貨交易。源代碼的最少改動原則是本項目初始的設計目標之一,當時是基于mt5源代碼考慮,更多的朋友建議:目標應該是適配mt4源代碼。好吧,回歸項目初心,繼續努力:MT4代碼適配器(CTPMQL4)來了。
邏輯上,CTPMQL4是記錄EA的持倉(報單)明細,MT5CTP是記錄持倉匯總。持倉明細和持倉匯總之間需要對賬,這是非常困難的,因為MT5CTP是一個開放的系統,持倉可以手動平倉,也可以外部軟件平倉,CTPMQL4的持倉明細對此并不知情,CTPMQL4持倉明細更像是封閉系統。后來朋友們建議,在完美適配和海量代碼的交易驅動之間選擇,后者更為核心。
CTPMQL4記錄持倉明細,并根據訂單的狀態自動更新,還需要做數據持久化,為此我們使用了數據庫來達到這個目標,因為數據庫操作關系持倉記錄的安全和準確,所以如果數據庫操作錯誤,CTPMQL4會發出Alert警告。數據庫中數據表結構如下:
關鍵字段的說明:
ticket:是訂單(報單)的系統唯一標識,用于標注分筆報單形成的持倉。原生系統這個字段來源于服務器,本項目中,我們使用了sqlite3數據庫的rowid這個隱形字段,這個字段是自增長的行序號,而且是單調遞增,可保證ticket本地唯一。
orderlots,openlots,closelots:三個數量分別用于表示持倉數量,開倉數量和平倉數量,一般來說,持倉數量是動態變化的,開倉數量和開倉數量是不變的,是初始報單的數量。也有特殊情況,比如撤單,報單錯誤,部分成交等等也會導致開倉數量和平倉數量的變化。這些變化不需要上層ea的關注,這些字段是用來組合判定是否是歷史倉,是否已經完全成交等信息。比如:orderlots=0,openlots>0表示掛單尚未成交;orderlots>0,openlots>0,orderlots<openlots表示訂單部分成交等等。CTP柜臺信息發生變化的時候,這些字段也在同步變化,并實時記錄到數據庫,這部分說明,只是幫助您理解系統字段的作用和工作邏輯,對上層ea是隱藏的。
opensysid,closesysid:這兩個字段是用來關聯ea的報單和CTP柜臺報單用的,用于響應CTP的報單回報信息。顯然,非CTPMQL4的報單,不會得到關注。
其他的字段大家應該都會比較熟悉,需要說明的是持倉的部分平倉,一筆持倉部分平倉后,我們的CTPMQL4會更新原持倉記錄為歷史持倉,未平倉部分形成一筆新的持倉,這個新的持倉使用了新的ticket,即數據庫新增了一條持倉記錄。
如何實現mt4源代碼的適配的呢?CTPMQL4使用了編譯預處理功能,對mt4的接口函數函數、預定義變量、枚舉變量等做了重定義,為此我們設計了一個類來打包。并在預處理開始的位置,實例化了類對象,因為編譯預處理的限制,部分函數需要在mt4源代碼中做顯式調用,這些內容你都可以查看CTPMQL4源碼找到對應的實現。特別需要聲明的就是指數合約的直接驅動交易,需要在ea層做設計處理,我們試圖在CTPMQL4中自動匹配處理這個問題,失敗了。
CTPMQL4支持mt4的所有報單類型,包括limit單和stop單。因為MT5CTP項目暫不支持期權,所以沒有實現mt4的OrderCloseBy函數。
CTPMQL4的設計理念、工作邏輯、架構和方法基本就是這樣的,接下來我們介紹如何使用?開始修改mt4的源代碼,直接驅動MT5CTP,總結下來就是五步,簡單到不可思議。
第一步:在mt4源代碼的最頂端,加上如下一段代碼:
解釋:定義__CTPMQL4__標識,打開CTPMQL4工作環境。
第二步:在OnTick回調函數的開始,加上如下一段代碼:
解釋:只需要紅色框中的這一句。類中調用,主要的工作有三點,一是完成初始化;二是填充類中的數據;三是持倉的止損止盈。
第三步:增加窗口事件回調,可以在代碼的最后,加上如下一段:
解釋:CTPMQL4需要跟蹤訂單的變化,并同步更新數據庫,記錄明細持倉的狀態,所以需要監聽CTP的訂單事件。
第四步:修改報單、平倉、撤單、訂單修改這幾個“主動”報單處理函數,做顯式調用:
解釋:所謂顯示調用就是在將OrderSend,OrderClose,OrderModify和OrderDelete函數替換成mt4.OrderSend,mt4.OrderClose,mt4.OrderModify和mt4.OrderDelete 。主要原因是預編譯處理器不支持函數嵌套,而我們使用這些函數的時候,往往會嵌套OrderTicket()等函數使用。
第五步:部分技術指標的顯式調用和iCustom自定義指標調用的特殊處理。顯式調用是因為預處理器支持的函數參數最多為8個,如果EA中調用的技術指標參數超過8個,你就需要顯式的調用CTPMQL4,就是在函數前面加上前綴“mt4.”。iCustom比較特殊,是個不定參數的函數,而這個函數又是如此重要,如果不能有合理的解決方案,CTPMQL4的可用性就大打折扣,這的確費了些腦筋。我們給出的方案是需要在mt4的源代碼中修改,使用MqlParam結構體數組將指標的參數填充好,然后再顯性調用mt4.iCustom。需要說明的是,你只需要填充參數部分就好,缺失的部分CTPMQL4又進一步做了處理,你可以查看此部分代碼,更深入的了解實現的方法。
一二三四五,簡單修改就可以使用原mt4的海量代碼,驅動MT5CTP進行國內的期貨交易。CTPMQL4是開源的,你可以按照自己的方式完善,變成你自己的獨一無二的驅動框架,獨門秘籍,so cool!
CTPMQL4秉持MT5CTP項目的一貫原則:Free Is Power!
補充介紹:iCustom的使用方法
介紹文檔中對iCustom和MqlParam結構體數組語焉不詳,結合代碼補充使用方法。假設我有一個自定義指標,名稱為:myMA(已在mt5環境下重新編譯無誤):
//+------------------------------------------------------------------+ //| Custom Moving Averages.mq4 | //| Copyright 2005-2015, MetaQuotes Software Corp. | //| http://www.mql4.com | //+------------------------------------------------------------------+ #property copyright "2005-2015, MetaQuotes Software Corp." #property link "http://www.mql4.com" #property description "Moving Average" #property strict#property indicator_chart_window #property indicator_buffers 1 #property indicator_plots 1 //--- plot ExtAMABuffer #property indicator_label1 "MA" #property indicator_type1 DRAW_LINE #property indicator_color1 Red #property indicator_style1 STYLE_SOLID #property indicator_width1 1 //【固定格式】包含合約庫文件/目錄 #ifndef __CTPMQL4__#define __CTPMQL4__#include <mt5ctpmtctp.mqh> #endif //--- indicator parameters input int InpMAPeriod=13; // Period input int InpMAShift=0; // Shift input ENUM_MA_METHOD InpMAMethod=MODE_SMA; // Method //--- indicator buffer double ExtLineBuffer[]; //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit(void){string short_name;int draw_begin=InpMAPeriod-1; //--- indicator short nameswitch(InpMAMethod){case MODE_SMA : short_name="SMA("; break;case MODE_EMA : short_name="EMA("; draw_begin=0; break;case MODE_SMMA : short_name="SMMA("; break;case MODE_LWMA : short_name="LWMA("; break;default : return(INIT_FAILED);}//--- check for inputif(InpMAPeriod<2)return(INIT_FAILED);//--- indicator buffers mappingSetIndexBuffer(0,ExtLineBuffer); //--- initialization donereturn(INIT_SUCCEEDED);} //+------------------------------------------------------------------+ //| Moving Average | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total,const int prev_calculated,const datetime &time[],const double &open[],const double &high[],const double &low[],const double &close[],const long &tick_volume[],const long &volume[],const int &spread[]){ //--- check for bars countif(rates_total<InpMAPeriod-1 || InpMAPeriod<2)return(0); //--- counting from 0 to rates_totalArraySetAsSeries(ExtLineBuffer,false);ArraySetAsSeries(close,false); //--- first calculation or number of bars was changedif(prev_calculated==0)ArrayInitialize(ExtLineBuffer,0); //--- calculationswitch(InpMAMethod){case MODE_EMA: CalculateEMA(rates_total,prev_calculated,close); break;case MODE_LWMA: CalculateLWMA(rates_total,prev_calculated,close); break;case MODE_SMMA: CalculateSmoothedMA(rates_total,prev_calculated,close); break;case MODE_SMA: CalculateSimpleMA(rates_total,prev_calculated,close); break;} //--- return value of prev_calculated for next callreturn(rates_total);} //+------------------------------------------------------------------+ //| simple moving average | //+------------------------------------------------------------------+ void CalculateSimpleMA(int rates_total,int prev_calculated,const double &price[]){int i,limit; //--- first calculation or number of bars was changedif(prev_calculated==0){limit=InpMAPeriod;//--- calculate first visible valuedouble firstValue=0;for(i=0; i<limit; i++)firstValue+=price[i];firstValue/=InpMAPeriod;ExtLineBuffer[limit-1]=firstValue;}elselimit=prev_calculated-1; //--- main loopfor(i=limit; i<rates_total && !IsStopped(); i++)ExtLineBuffer[i]=ExtLineBuffer[i-1]+(price[i]-price[i-InpMAPeriod])/InpMAPeriod; //---} //+------------------------------------------------------------------+ //| exponential moving average | //+------------------------------------------------------------------+ void CalculateEMA(int rates_total,int prev_calculated,const double &price[]){int i,limit;double SmoothFactor=2.0/(1.0+InpMAPeriod); //--- first calculation or number of bars was changedif(prev_calculated==0){limit=InpMAPeriod;ExtLineBuffer[0]=price[0];for(i=1; i<limit; i++)ExtLineBuffer[i]=price[i]*SmoothFactor+ExtLineBuffer[i-1]*(1.0-SmoothFactor);}elselimit=prev_calculated-1; //--- main loopfor(i=limit; i<rates_total && !IsStopped(); i++)ExtLineBuffer[i]=price[i]*SmoothFactor+ExtLineBuffer[i-1]*(1.0-SmoothFactor); //---} //+------------------------------------------------------------------+ //| linear weighted moving average | //+------------------------------------------------------------------+ void CalculateLWMA(int rates_total,int prev_calculated,const double &price[]){int i,limit;static int weightsum;double sum; //--- first calculation or number of bars was changedif(prev_calculated==0){weightsum=0;limit=InpMAPeriod;//--- calculate first visible valuedouble firstValue=0;for(i=0;i<limit;i++){int k=i+1;weightsum+=k;firstValue+=k*price[i];}firstValue/=(double)weightsum;ExtLineBuffer[limit-1]=firstValue;}elselimit=prev_calculated-1; //--- main loopfor(i=limit; i<rates_total && !IsStopped(); i++){sum=0;for(int j=0;j<InpMAPeriod;j++)sum+=(InpMAPeriod-j)*price[i-j];ExtLineBuffer[i]=sum/weightsum;} //---} //+------------------------------------------------------------------+ //| smoothed moving average | //+------------------------------------------------------------------+ void CalculateSmoothedMA(int rates_total,int prev_calculated,const double &price[]){int i,limit; //--- first calculation or number of bars was changedif(prev_calculated==0){limit=InpMAPeriod;double firstValue=0;for(i=0; i<limit; i++)firstValue+=price[i];firstValue/=InpMAPeriod;ExtLineBuffer[limit-1]=firstValue;}elselimit=prev_calculated-1; //--- main loopfor(i=limit; i<rates_total && !IsStopped(); i++)ExtLineBuffer[i]=(ExtLineBuffer[i-1]*(InpMAPeriod-1)+price[i])/InpMAPeriod; //---} //+------------------------------------------------------------------+調用的方式,直接給出代碼如下:
//【固定格式】包含合約庫文件/目錄 #ifndef __CTPMQL4__#define __CTPMQL4__#include <mt5ctpmtctp.mqh> #endif //--- input indi name input string InpName="myMA.mq4"; // 自定義指標名稱 //--- indicator parameters input int InpMAPeriod=13; // Period input int InpMAShift=0; // Shift input ENUM_MA_METHOD InpMAMethod=MODE_SMA; // Method // 全局變量/保存自定義指標參數及類型 MqlParam para[3]; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() {// 設置指標參數:3個 //參數1para[0].type = TYPE_INT; //參數類型para[0].integer_value = InpMAPeriod; //參數值//參數2para[1].type = TYPE_INT; //參數類型para[1].integer_value = InpMAShift; //參數值//參數3para[2].type = TYPE_INT; //參數類型para[2].integer_value = InpMAMethod; //參數值return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() {mt4.OnTick();//-- 調用自定義指標double my_ma = mt4.iCustom(NULL,0,InpName,para,0,0);double pre_my_ma = mt4.iCustom(NULL,0,InpName,para,0,1);// do...// 調用Print()|comment()函數,信息輸出/檢查是個好習慣 } //+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam) {mt4.OnChartEvent(id,lparam,dparam,sparam); } //+------------------------------------------------------------------+自定義指標的調用難的不是參數表重整和EA調用,修改自定義指標到mt5中或許更復雜。困難的地方在于指標的編寫表達方式mt4和mt5有很大的差異,很多函數的調用都是不一致的,另外就是指標中調用指標或函數,會成倍的增加版本升級的難度。
上次測試用demo也放到這里,僅供參考。
demo1:Moving Average
//+------------------------------------------------------------------+ //| Moving Average.mq4 | //| Copyright 2005-2014, MetaQuotes Software Corp. | //| http://www.mql4.com | //+------------------------------------------------------------------+ #property copyright "2005-2014, MetaQuotes Software Corp." #property link "http://www.mql4.com" #property description "Moving Average sample expert advisor"#ifndef __CTPMQL4__#define __CTPMQL4__#include <mt5ctpmtctp.mqh> #endif#define MAGICMA 20131111 //--- Inputs input double Lots =0.1; input double MaximumRisk =0.02; input double DecreaseFactor=3; input int MovingPeriod =12; input int MovingShift =6; //+------------------------------------------------------------------+ //| Calculate open positions | //+------------------------------------------------------------------+ int CalculateCurrentOrders(string symbol){int buys=0,sells=0; //---int orders = OrdersTotal();for(int i=orders-1;i>=0;i--){if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES)==false) break;if(OrderSymbol()==Symbol() && OrderMagicNumber()==MAGICMA){if(OrderType()==OP_BUY) buys++;if(OrderType()==OP_SELL) sells++;}} //--- return orders volumeif(buys>0) return(buys);else return(-sells);} //+------------------------------------------------------------------+ //| Calculate optimal lot size | //+------------------------------------------------------------------+ double LotsOptimized(){double lot=Lots;int orders=OrdersHistoryTotal(); // history orders totalint losses=0; // number of losses orders without a break //--- select lot sizelot=NormalizeDouble(AccountFreeMargin()*MaximumRisk/1000.0,1); //--- calcuulate number of losses orders without a breakif(DecreaseFactor>0){for(int i=orders-1;i>=0;i--){if(OrderSelect(i,SELECT_BY_POS,MODE_HISTORY)==false){Print("Error in history!");break;}if(OrderSymbol()!=Symbol() || OrderType()>OP_SELL)continue;//---if(OrderProfit()>0) break;if(OrderProfit()<0) losses++;}if(losses>1)lot=NormalizeDouble(lot-lot*losses/DecreaseFactor,1);} //--- return lot sizeif(lot<0.1) lot=0.1;return(lot);} //+------------------------------------------------------------------+ //| Check for open order conditions | //+------------------------------------------------------------------+ void CheckForOpen(){double ma;int res; //--- go trading only for first tiks of new barif(Volume[0]>1) return; //--- get Moving Average ma=iMA(NULL,0,MovingPeriod,MovingShift,MODE_SMA,PRICE_CLOSE,0); //--- sell conditionsif(Open[1]>ma && Close[1]<ma){res=mt4.OrderSend(Symbol(),OP_SELL,LotsOptimized(),Bid,3,0,0,"mtctp測試",MAGICMA,0,Red);return;} //--- buy conditionsif(Open[1]<ma && Close[1]>ma){res=mt4.OrderSend(Symbol(),OP_BUY,LotsOptimized(),Ask,3,0,0,"mtctp測試",MAGICMA,0,Blue);return;} //---} //+------------------------------------------------------------------+ //| Check for close order conditions | //+------------------------------------------------------------------+ void CheckForClose(){double ma; //--- go trading only for first tiks of new barif(Volume[0]>1) return; //--- get Moving Average ma=iMA(NULL,0,MovingPeriod,MovingShift,MODE_SMA,PRICE_CLOSE,0); //---int orders = OrdersTotal();for(int i = orders-1;i>=0;i--){if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES)==false) break;if(OrderMagicNumber()!=MAGICMA || OrderSymbol()!=Symbol()) continue;//--- check order type if(OrderType()==OP_BUY){if(Open[1]>ma && Close[1]<ma){if(!mt4.OrderClose(OrderTicket(),OrderLots(),Bid,3,White))Print("OrderClose error ",GetLastError());}break;}if(OrderType()==OP_SELL){if(Open[1]<ma && Close[1]>ma){if(!mt4.OrderClose(OrderTicket(),OrderLots(),Ask,3,White))Print("OrderClose error ",GetLastError());}break;}} //---} //+------------------------------------------------------------------+ //| OnTick function | //+------------------------------------------------------------------+ void OnTick(){mt4.OnTick(); //--- check for history and tradingif(Bars<100 || IsTradeAllowed()==false)return; //--- calculate open orders by current symbolif(CalculateCurrentOrders(Symbol())==0) CheckForOpen();else CheckForClose(); //---} //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam){mt4.OnChartEvent(id,lparam,dparam,sparam);} //+------------------------------------------------------------------+demo2:MACD Sample
//+------------------------------------------------------------------+ //| MACD Sample.mq4 | //+------------------------------------------------------------------+ #ifndef __CTPMQL4__#define __CTPMQL4__#include <mt5ctpmtctp.mqh> #endifinput double TakeProfit =50; input double Lots =0.1; input double TrailingStop =30; input double MACDOpenLevel =3; input double MACDCloseLevel=2; input int MATrendPeriod =26; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnTick(void){mt4.OnTick();double MacdCurrent,MacdPrevious;double SignalCurrent,SignalPrevious;double MaCurrent,MaPrevious;int cnt,ticket,total; //--- // initial data checks // it is important to make sure that the expert works with a normal // chart and the user did not make any mistakes setting external // variables (Lots, StopLoss, TakeProfit, // TrailingStop) in our case, we check TakeProfit // on a chart of less than 100 bars //---if(Bars<100){Print("bars less than 100");return;}if(TakeProfit<10){Print("TakeProfit less than 10");return;} //--- to simplify the coding and speed up access data are put into internal variablesMacdCurrent=iMACD(NULL,0,12,26,9,PRICE_CLOSE,MODE_MAIN,0);MacdPrevious=iMACD(NULL,0,12,26,9,PRICE_CLOSE,MODE_MAIN,1);SignalCurrent=iMACD(NULL,0,12,26,9,PRICE_CLOSE,MODE_SIGNAL,0);SignalPrevious=iMACD(NULL,0,12,26,9,PRICE_CLOSE,MODE_SIGNAL,1);MaCurrent=iMA(NULL,0,MATrendPeriod,0,MODE_EMA,PRICE_CLOSE,0);MaPrevious=iMA(NULL,0,MATrendPeriod,0,MODE_EMA,PRICE_CLOSE,1);total=OrdersTotal();if(total<1){//--- no opened orders identifiedif(AccountFreeMargin()<(1000*Lots)){Print("We have no money. Free Margin = ",AccountFreeMargin());return;}//--- check for long position (BUY) possibilityif(MacdCurrent<0 && MacdCurrent>SignalCurrent && MacdPrevious<SignalPrevious && MathAbs(MacdCurrent)>(MACDOpenLevel*Point) && MaCurrent>MaPrevious){ticket=mt4.OrderSend(Symbol(),OP_BUY,Lots,Ask,3,0,Ask+TakeProfit*Point,"macd sample",16384,0,Green);if(ticket>0){if(OrderSelect(ticket,SELECT_BY_TICKET,MODE_TRADES))Print("BUY order opened : ",OrderOpenPrice());}elsePrint("Error opening BUY order : ",GetLastError());return;}//--- check for short position (SELL) possibilityif(MacdCurrent>0 && MacdCurrent<SignalCurrent && MacdPrevious>SignalPrevious && MacdCurrent>(MACDOpenLevel*Point) && MaCurrent<MaPrevious){ticket=mt4.OrderSend(Symbol(),OP_SELL,Lots,Bid,3,0,Bid-TakeProfit*Point,"macd sample",16384,0,Red);if(ticket>0){if(OrderSelect(ticket,SELECT_BY_TICKET,MODE_TRADES))Print("SELL order opened : ",OrderOpenPrice());}elsePrint("Error opening SELL order : ",GetLastError());}//--- exit from the "no opened orders" blockreturn;} //--- it is important to enter the market correctly, but it is more important to exit it correctly... for(cnt=0;cnt<total;cnt++){if(!OrderSelect(cnt,SELECT_BY_POS,MODE_TRADES))continue;if(OrderType()<=OP_SELL && // check for opened position OrderSymbol()==Symbol()) // check for symbol{//--- long position is openedif(OrderType()==OP_BUY){//--- should it be closed?if(MacdCurrent>0 && MacdCurrent<SignalCurrent && MacdPrevious>SignalPrevious && MacdCurrent>(MACDCloseLevel*Point)){//--- close order and exitif(!mt4.OrderClose(OrderTicket(),OrderLots(),Bid,3,Violet))Print("OrderClose error ",GetLastError());return;}//--- check for trailing stopif(TrailingStop>0){if(Bid-OrderOpenPrice()>Point*TrailingStop){if(OrderStopLoss()<Bid-Point*TrailingStop){//--- modify order and exitif(!mt4.OrderModify(OrderTicket(),OrderOpenPrice(),Bid-Point*TrailingStop,OrderTakeProfit(),0,Green))Print("OrderModify error ",GetLastError());return;}}}}else // go to short position{//--- should it be closed?if(MacdCurrent<0 && MacdCurrent>SignalCurrent && MacdPrevious<SignalPrevious && MathAbs(MacdCurrent)>(MACDCloseLevel*Point)){//--- close order and exitif(!mt4.OrderClose(OrderTicket(),OrderLots(),Ask,3,Violet))Print("OrderClose error ",GetLastError());return;}//--- check for trailing stopif(TrailingStop>0){if((OrderOpenPrice()-Ask)>(Point*TrailingStop)){if((OrderStopLoss()>(Ask+Point*TrailingStop)) || (OrderStopLoss()==0)){//--- modify order and exitif(!mt4.OrderModify(OrderTicket(),OrderOpenPrice(),Ask+Point*TrailingStop,OrderTakeProfit(),0,Red))Print("OrderModify error ",GetLastError());return;}}}}}} //---} //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam){mt4.OnChartEvent(id,lparam,dparam,sparam);} //+------------------------------------------------------------------+總結
以上是生活随笔為你收集整理的如何查看eas源代码_MT5CTP扩展:MT4源代码(EA)适配器来了的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: cdh sqoop 配置_相比于手动搭建
- 下一篇: 测试用例 集成测试增删改查_20年高级测