• 在c++中使用Outlook Object Model发送邮件


    一、Outlook Object Model简介

      Outlook Object Model(OOM)是outlook为开发者提供的一个COM组件,我们可以在程序中使用它来发送邮件、管理邮箱等。相关介绍可以参见以下链接:

      https://msdn.microsoft.com/en-us/library/ms268893.aspx

      可能有人会说用shellExcute也可以实现对outlook的操作,这里正好说到为什么要用Outlook Object Model的原因之一。如果用shellExcute来操作,必须严格保证传入的参数的编码和长宽字节符合outlook的要求,也就是说在进行实际的邮件操作之前需要对字符串参数进行繁琐的判断和处理(如果是自己写的小程序玩一玩的请随意),而用OOM来操作则不需要关心这些问题。

      OOM的另外一个优点就是接口简单,使用方便。要兼容所有的outlook版本也比较方便,这个下面再说。

    二、在c++中的使用方式

      因为微软的官网上关于OOM的例程主要是VBA和c#的,相关的例子有很多,这里就不多介绍了,主要介绍怎么在c++中使用。

      可以参见下面这个链接:

      http://1code.codeplex.com/SourceControl/changeset/view/60353#585447

      主要有三种方式来使用OOM:

      1、在程序中用#import命令来导入OOM的类型库,然后使用c++里的智能指针来调用相关的函数和属性。例程如下:

     1 #import "C:Program FilesCommon FilesMicrosoft SharedOFFICE15mso.dll" no_namespace  
     2         rename("DocumentProperties","_DocumentProperties") 
     3         rename("RGB", "MsoRGB") 
     4 
     5 #import "D:officeOffice15MSOUTL.OLB"      
     6         rename("GetOrganizer", "GetOrganizerAE")
     7         rename_namespace("Outlook")
     8 
     9 using namespace Outlook;
    10 void SendMail_UsingOOM(const std::wstring &, const std::wstring &, const std::wstring &, const std::wstring &, const std::wstring &, const std::wstring &, bool);
    11 int main()
    12 {
    13     SendMail_UsingOOM(L"a@email.com", L"aa@email.com", L"aaa@email.com", L"OOM_Test", L"It`s OOM Test.Do not care about it.", L"C:\Users\Desktop\aaa.pdf", true);
    14     return 0;
    15 }
    16 
    17 void SendMail_UsingOOM(const std::wstring &to,const std::wstring &cc,const std::wstring &bcc,const std::wstring &subject,const std::wstring &body,const std::wstring &attachmentPath,bool showUI)
    18 {
    19     CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
    20     try
    21     {
    22         _ApplicationPtr spApp("Outlook.Application");
    23         _NameSpacePtr pMAPI = spApp->GetNamespace("MAPI");
    24         pMAPI->Logon();
    25 
    26         _MailItemPtr olMail(spApp->CreateItem(olMailItem));
    27         if (0 == to.size())
    28         {
    29             pMAPI->Logoff();
    30             return;
    31         }
    32         olMail->PutTo(_bstr_t(to.c_str()));
    33         olMail->PutCC(_bstr_t(cc.c_str()));
    34         olMail->PutBCC(_bstr_t(bcc.c_str()));
    35         olMail->PutSubject(_bstr_t(subject.c_str()));
    36         olMail->PutBody(_bstr_t(body.c_str()));
    37         olMail->Save();
    38 
    39         if (0 != attachmentPath.size())
    40         {
    41             AttachmentsPtr olAtta = olMail->GetAttachments();
    42             olAtta->Add(attachmentPath.c_str(), 1, 1, attachmentPath.c_str());
    43         }
    44         HRESULT result = NULL;
    45         if (showUI)
    46         {
    47             result = olMail->Display(showUI);
    48         }
    49         else
    50         {
    51             result = olMail->Send();
    52         }
    53         pMAPI->Logoff();
    54     }
    55     catch (_com_error &err)
    56     {
    57         wprintf(L"Outlook throws the error: %s
    ", err.ErrorMessage());
    58         wprintf(L"Description: %s
    ", (LPCWSTR)err.Description());
    59     }
    60     CoUninitialize();
    61 }

      要注意,这里的智能指针并不是你自己定义的,而是COM组件提供的。比如上面程序里的_ApplicationPtr、_NameSpacePtr等,因为是智能指针,也不需要手动释放,非常方便。

      这里需要导入两个类型库文件:mso.dll和msoutl.olb,前者是office的,后者是outlook的。在导入后会生成mso.tlh、mso.tli、msoutl.tlh和msoutl.tli,tlh文件就相当于头文件,tli文件相当于cpp文件。实际上这个生成的过程只是最开始的时候执行了一次,生成的tlh、tli文件完全可以放到其他项目中包含(可能会产生重名问题,需要手动更改)。

      在实际过程中,会发现import可能出现很多乱七八糟的错误,比如这样:

    1 c:OutlookAdd-InOutlookAdd-Indebugmsoutl.tlh(20855) : error C2556: 'Outlook::AddressEntryPtr Outlook::_AppointmentItem::GetOrganizer(void)' : overloaded function differs only by return type from '_bstr_t Outlook::_AppointmentItem::GetOrganizer(void)'
    2 c:OutlookAdd-InOutlookAdd-Indebugmsoutl.tlh(20753) : see declaration of 'Outlook::_AppointmentItem::GetOrganizer'

      这其实是方法名冲突的问题,上面这个例子是GetOrganizer(void)这个方法冲突了,可以用rename关键字来修改namespace里实际生成的方法名以避免冲突。这里可以参见以下链接:

      https://social.msdn.microsoft.com/Forums/office/en-US/f51cde52-0faa-42a2-bf61-c18b6f5b0e64/error-while-building-the-outlook-addin-code-with-ms-outlook-2010?forum=outlookdev

      http://blog.chinaunix.net/uid-20791902-id-292054.html

      2、第二种方式是使用COM组件提供的API来调用相关的函数,这种方式需要我们使用特定的API函数,将要调用的方法名和相关参数作为参数传入这些API中。例程如下:

      1 // laterbind.cpp : 定义控制台应用程序的入口点。
      2 //
      3 
      4 #include "stdafx.h"
      5 struct Wrap_errno
      6 {
      7     HRESULT hr;
      8     EXCEPINFO info;
      9 };
     10 Wrap_errno AutoWrap(int autoType, VARIANT *pvResult, IDispatch *pDisp,
     11     LPOLESTR ptName, int cArgs...)
     12 {
     13     // Begin variable-argument list
     14     va_list marker;
     15     va_start(marker, cArgs);
     16 
     17     Wrap_errno err;
     18     memset(&err, 0, sizeof err);
     19 
     20     if (!pDisp)
     21     {
     22         err.hr = E_INVALIDARG;
     23         return err;
     24     }
     25 
     26     // Variables used
     27     DISPPARAMS dp = { NULL, NULL, 0, 0 };
     28     DISPID dispidNamed = DISPID_PROPERTYPUT;
     29     DISPID dispID;
     30     HRESULT hr;
     31 
     32     // Get DISPID for name passed
     33     hr = pDisp->GetIDsOfNames(IID_NULL, &ptName, 1, LOCALE_USER_DEFAULT, &dispID);
     34     if (FAILED(hr))
     35     {
     36         err.hr = hr;
     37         return err;
     38     }
     39 
     40     // Allocate memory for arguments
     41     VARIANT *pArgs = new VARIANT[cArgs + 1];
     42     // Extract arguments...
     43     for (int i = 0; i < cArgs; i++)
     44     {
     45         pArgs[i] = va_arg(marker, VARIANT);
     46     }
     47 
     48     // Build DISPPARAMS
     49     dp.cArgs = cArgs;
     50     dp.rgvarg = pArgs;
     51 
     52     // Handle special-case for property-puts
     53     if (autoType & DISPATCH_PROPERTYPUT)
     54     {
     55         dp.cNamedArgs = 1;
     56         dp.rgdispidNamedArgs = &dispidNamed;
     57     }
     58 
     59     // Make the call
     60     EXCEPINFO excepInfo;
     61     memset(&excepInfo, 0, sizeof excepInfo);
     62     hr = pDisp->Invoke(dispID, IID_NULL, LOCALE_SYSTEM_DEFAULT,
     63         autoType, &dp, pvResult, &excepInfo, NULL);
     64     if (FAILED(hr))
     65     {
     66         err.hr = hr;
     67         err.info = excepInfo;
     68         delete[] pArgs;
     69         return err;
     70     }
     71 
     72     // End variable-argument section
     73     va_end(marker);
     74 
     75     delete[] pArgs;
     76 
     77     return err;
     78 }
     79 
     80 Wrap_errno SendMail_UsingOOM(
     81     const std::wstring &to,
     82     const std::wstring &cc,
     83     const std::wstring &bcc,
     84     const std::wstring &subject,
     85     const std::wstring &body,
     86     const std::wstring &attachmentPath,
     87     bool showUI
     88     )
     89 {
     90     CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
     91 
     92     // Define vtMissing for optional parameters in some calls.
     93     VARIANT vtMissing;
     94     vtMissing.vt = VT_EMPTY;
     95 
     96     CLSID clsid;
     97     HRESULT hr;
     98 
     99     Wrap_errno err;
    100     memset(&err, 0, sizeof err);
    101 
    102     LPCOLESTR progID = L"Outlook.Application";
    103     hr = CLSIDFromProgID(progID, &clsid);
    104     if (FAILED(hr))
    105     {
    106         CoUninitialize();
    107         return err;
    108     }
    109     IDispatch *pOutlookApp = NULL;
    110     hr = CoCreateInstance(
    111         clsid,                    // CLSID of the server
    112         NULL,
    113         CLSCTX_LOCAL_SERVER,    // Outlook.Application is a local server
    114         IID_IDispatch,            // Query the IDispatch interface
    115         (void **)&pOutlookApp);    // Output
    116 
    117     if (FAILED(hr))
    118     {
    119         if (pOutlookApp != NULL)
    120         {
    121             pOutlookApp->Release();
    122         }
    123         CoUninitialize();
    124         return err;
    125     }
    126 
    127     // pNS = pOutlookApp->GetNamespace("MAPI")
    128     IDispatch *pNS = NULL;
    129     {
    130         VARIANT x;
    131         x.vt = VT_BSTR;
    132         x.bstrVal = SysAllocString(L"MAPI");
    133 
    134         VARIANT result;
    135         VariantInit(&result);
    136         err = AutoWrap(DISPATCH_METHOD, &result, pOutlookApp, L"GetNamespace", 1, x);
    137         if (err.hr < 0)
    138         {
    139             VariantClear(&result);
    140             VariantClear(&x);
    141             if (pOutlookApp != NULL)
    142             {
    143                 pOutlookApp->Release();
    144             }
    145             if (pNS != NULL)
    146             {
    147                 pNS->Release();
    148             }
    149             CoUninitialize();
    150             return err;
    151         }
    152         pNS = result.pdispVal;
    153         VariantClear(&x);
    154     }
    155 
    156     // pNS->Logon(vtMissing, vtMissing, true, true)
    157     {
    158         VARIANT vtShowDialog;
    159         vtShowDialog.vt = VT_BOOL;
    160         vtShowDialog.boolVal = VARIANT_TRUE;
    161         VARIANT vtNewSession;
    162         vtNewSession.vt = VT_BOOL;
    163         vtNewSession.boolVal = VARIANT_TRUE;
    164 
    165         AutoWrap(DISPATCH_METHOD, NULL, pNS, L"Logon", 4, vtNewSession,
    166             vtShowDialog, vtMissing, vtMissing);
    167     }
    168 
    169     // pMail = pOutlookApp->CreateItem(Outlook::olMailItem);
    170     IDispatch *pMail = NULL;
    171     {
    172         VARIANT x;
    173         x.vt = VT_I4;
    174         x.lVal = 0;     // Outlook::olMailItem
    175 
    176         VARIANT result;
    177         VariantInit(&result);
    178         err = AutoWrap(DISPATCH_METHOD, &result, pOutlookApp, L"CreateItem", 1, x);
    179         if (err.hr < 0)
    180         {
    181             if (pMail != NULL)
    182             {
    183                 pMail->Release();
    184             }
    185             if (pNS != NULL)
    186             {
    187                 pNS->Release();
    188             }
    189             if (pOutlookApp != NULL)
    190             {
    191                 pOutlookApp->Release();
    192             }
    193             VariantClear(&x);
    194             VariantClear(&result);
    195             CoUninitialize();
    196             return err;
    197         }
    198         pMail = result.pdispVal;
    199     }
    200 
    201     // pMail->Subject = _bstr_t(L"Feedback of All-In-One Code Framework");
    202     {
    203         VARIANT x;
    204         x.vt = VT_BSTR;
    205         x.bstrVal = SysAllocString(subject.c_str());
    206         AutoWrap(DISPATCH_PROPERTYPUT, NULL, pMail, L"Subject", 1, x);
    207         VariantClear(&x);
    208     }
    209 
    210     // pMail->To = _bstr_t(L"codefxf@microsoft.com");
    211     {
    212         VARIANT x;
    213         x.vt = VT_BSTR;
    214         x.bstrVal = SysAllocString(to.c_str());
    215         AutoWrap(DISPATCH_PROPERTYPUT, NULL, pMail, L"To", 1, x);
    216         VariantClear(&x);
    217     }
    218 
    219     // pMail->cc 
    220     {
    221         VARIANT x;
    222         x.vt = VT_BSTR;
    223         x.bstrVal = SysAllocString(cc.c_str());
    224         AutoWrap(DISPATCH_PROPERTYPUT, NULL, pMail, L"Cc", 1, x);
    225         VariantClear(&x);
    226     }
    227 
    228     // pMail->bcc
    229     {
    230         VARIANT x;
    231         x.vt = VT_BSTR;
    232         x.bstrVal = SysAllocString(bcc.c_str());
    233         AutoWrap(DISPATCH_PROPERTYPUT, NULL, pMail, L"Bcc", 1, x);
    234         VariantClear(&x);
    235     }
    236 
    237     // pMail->body
    238     {
    239         VARIANT x;
    240         x.vt = VT_BSTR;
    241         x.bstrVal = SysAllocString(body.c_str());
    242         AutoWrap(DISPATCH_PROPERTYPUT, NULL, pMail, L"Body", 1, x);
    243         VariantClear(&x);
    244     }
    245 
    246     // pAtta = pMail->GetAttachments
    247     // pAtta->Add(source,type,position,displayname)
    248     IDispatch *pAtta = NULL;
    249     {
    250         VARIANT result;
    251         VariantInit(&result);
    252         err = AutoWrap(DISPATCH_PROPERTYGET, &result, pMail, L"Attachments", 0);
    253         if (err.hr < 0)
    254         {
    255             if (pMail != NULL)
    256             {
    257                 pMail->Release();
    258             }
    259             if (pNS != NULL)
    260             {
    261                 pNS->Release();
    262             }
    263             if (pOutlookApp != NULL)
    264             {
    265                 pOutlookApp->Release();
    266             }
    267             VariantClear(&result);
    268             CoUninitialize();
    269             return err;
    270         }
    271         pAtta = result.pdispVal;
    272 
    273         VARIANT path;
    274         path.vt = VT_BSTR;
    275         path.bstrVal = SysAllocString(attachmentPath.c_str());
    276         VARIANT x;
    277         x.vt = VT_I4;
    278         x.lVal = 1;
    279         err = AutoWrap(DISPATCH_METHOD, NULL, pAtta, L"Add", 4, path, x, x, path);
    280         if (err.hr < 0)
    281         {
    282             if (pAtta != NULL)
    283             {
    284                 pAtta->Release();
    285             }
    286             if (pMail != NULL)
    287             {
    288                 pMail->Release();
    289             }
    290             if (pNS != NULL)
    291             {
    292                 pNS->Release();
    293             }
    294             if (pOutlookApp != NULL)
    295             {
    296                 pOutlookApp->Release();
    297             }
    298             VariantClear(&result);
    299             VariantClear(&path);
    300             CoUninitialize();
    301             return err;
    302         }
    303         VariantClear(&path);
    304     }
    305 
    306     // pMail->Display(true);
    307     {
    308         VARIANT vtModal;
    309         vtModal.vt = VT_BOOL;
    310         vtModal.boolVal = VARIANT_TRUE;
    311         err = AutoWrap(DISPATCH_METHOD, NULL, pMail, L"Display", 1, vtModal);
    312         if (err.hr < 0)
    313         {
    314             if (pMail != NULL)
    315             {
    316                 pMail->Release();
    317             }
    318             if (pNS != NULL)
    319             {
    320                 pNS->Release();
    321             }
    322             if (pOutlookApp != NULL)
    323             {
    324                 pOutlookApp->Release();
    325             }
    326             VariantClear(&vtModal);
    327             CoUninitialize();
    328             return err;
    329         }
    330     }
    331 
    332     // pNS->Logoff()
    333     err = AutoWrap(DISPATCH_METHOD, NULL, pNS, L"Logoff", 0);
    334     if (err.hr < 0)
    335     {
    336         if (pMail != NULL)
    337         {
    338             pMail->Release();
    339         }
    340         if (pNS != NULL)
    341         {
    342             pNS->Release();
    343         }
    344         if (pOutlookApp != NULL)
    345         {
    346             pOutlookApp->Release();
    347         }
    348         CoUninitialize();
    349         return err;
    350     }
    351 
    352     if (pMail != NULL)
    353     {
    354         pMail->Release();
    355     }
    356     if (pNS != NULL)
    357     {
    358         pNS->Release();
    359     }
    360     if (pOutlookApp != NULL)
    361     {
    362         pOutlookApp->Release();
    363     }
    364 
    365     CoUninitialize();
    366     err.hr = S_OK;
    367     return err;
    368 }
    369 int main()
    370 {
    371     SendMail_UsingOOM(L"aaa@email.com", L"aaaa@email.com", L"aaaa@email.com", L"OOM_Test", L"It`s OOM Test.Do not care it.", L"C:\Users\Desktop\aaa.pdf", true);
    372     return 0;
    373 }

      这里人工地把调用相关的API封装在了AutoWrap函数里,通过调用该函数即可调用相关操作的函数,可以看到主要的API就是CLSIDFromProgID、CoCreateInstance、GetIDsOfNames、Invoke这些COM函数,不熟悉的同学可以去看下com操作相关的资料。

      要使用上面这种方式来操作OOM,你需要首先知道OOM提供的接口以及不同接口、不同属性之间的关系,因为你是通过com的API来调用OOM,它并没有生成外部代码,所以接口的逻辑在外部是不可见的。我们可以通过微软的官网来查看OOM的接口:

      https://msdn.microsoft.com/en-us/library/office/microsoft.office.interop.outlook.aspx

      

      3、第三种使用方式是在MFC工程中使用,依次打开MFC里的类向导-添加类-类型库中的MFC类,如下:

    将msoutl.olb中的相关接口导入工程中,可以用哪个就导入哪个接口,类向导会为每个接口都生成一个类。一般是在接口的名字里以“C”代替“_”来命名。如下图:

    之后我们就可以以类的形式来使用这些接口了,非常方便。例程如下:

     1 void CAboutDlg::SendMail_UsingOOM(
     2     const std::wstring &to,
     3     const std::wstring &cc,
     4     const std::wstring &bcc,
     5     const std::wstring &subject,
     6     const std::wstring &body,
     7     const std::wstring &attachmentPath,
     8     bool showUI
     9     )
    10 {
    11     try
    12     {
    13         CApplication olApp;
    14         COleException e;
    15         if (!olApp.CreateDispatch(L"Outlook.Application", &e)) {
    16             CString str;
    17             str.Format(L"CreateDispatch() failed w/error 0x%08lx", e.m_sc);
    18             AfxMessageBox(str, MB_SETFOREGROUND);
    19             return;
    20         }
    21         // Logon. Doesn't hurt if you are already running and logged on...
    22         CNameSpace olNs(olApp.GetNamespace(L"MAPI"));
    23         COleVariant covOptional((long)DISP_E_PARAMNOTFOUND, VT_ERROR);
    24         olNs.Logon(covOptional, covOptional, covOptional, covOptional);
    25 
    26         // Prepare a new mail message
    27         CMailItem olMail(olApp.CreateItem(0));
    28 
    29         if (0 != to.size())
    30             olMail.put_To(to.c_str());
    31         else
    32         {
    33             olNs.Logoff();
    34             return;
    35         }
    36 
    37         if (0 != subject.size())olMail.put_Subject(subject.c_str());
    38         if (0 != body.size())olMail.put_Body(body.c_str());
    39         if (0 != cc.size())olMail.put_CC(cc.c_str());
    40         if (0 != bcc.size())olMail.put_BCC(bcc.c_str());
    41 
    42         olMail.Save();
    43 
    44         if (0 != attachmentPath.size())
    45         {
    46             CAttachments olAtta(olMail.get_Attachments());
    47             VARIANT vFile;
    48             vFile = _variant_t(attachmentPath.c_str());//COleVariant(attachmentPath.c_str());
    49             VARIANT vType;
    50             vType.vt = VT_I4;
    51             vType.lVal = 1;
    52             VARIANT vPosition;
    53             vPosition.vt = VT_I4;
    54             vPosition.lVal = 1;
    55             VARIANT vNick;
    56             vNick.vt = VT_BSTR;
    57             vNick.bstrVal = SysAllocString(attachmentPath.c_str());
    58             olAtta.Add(vFile, vType, vPosition, vNick);
    59             SysFreeString(vNick.bstrVal);
    60         }
    61 
    62         if (showUI)
    63         {
    64             VARIANT vParam;
    65             vParam.vt = VT_BOOL;
    66             vParam.boolVal = showUI;
    67             olMail.Display(vParam);
    68         }
    69         else
    70         {
    71             // Send the message!
    72             olMail.Send();
    73         }
    74         olNs.Logoff();
    75         //olApp.Quit();
    76     }
    77     catch (_com_error &err)
    78     {
    79         MessageBox(L"Outlook throws the error: %s
    ", err.ErrorMessage());
    80         MessageBox(L"Description: %s
    ", (LPCWSTR)err.Description());
    81     }
    82 }

    在COM里面传递的数据全部要通过VARIANT的类型来传递,它的vt属性表示传递的数据类型,不同的数据类型要用不同的联合属性表示,对应关系参见以下链接:

      http://blog.csdn.net/heaven13483/article/details/8259110

    三、eraly binding和late binding

      有的同学说:在非MFC的c++项目里,用第一种方法就够了,干嘛要介绍第二种呢。的确,第一种方法有生成好的接口,方便我们理清接口逻辑关系、进行调试和维护,这种在导入后就已经生成固定接口的方式叫做early binding。程序像调用普通函数一样,在调用时知道这个函数的功能和逻辑,能够进行类型检查等安全措施。而第二种方法里,程序只是调用API告诉com组件我要调用哪个函数,至于它具体执行的功能程序并不知道,这就叫做late binding,也可以叫做动态绑定。

      early binding确实有很多优点,由于接口在导入后就已经固定,它的速度比late binding快一倍;它能进行类型检查方便维护;它的逻辑代码简单......它的缺点是导入类型库并不稳定,有时候总是会出现各种错误。另外还有outlook版本兼容性的问题,这个下面再谈。

      使用late binding的优点正是不需要特殊的类型库或者头文件,直接调用com的API就可以了,也就是说,这种方法在哪都能用。

      详细地介绍可以参考以下链接:

      https://msdn.microsoft.com/en-us/library/office/bb610234.aspx?f=255&MSPPError=-2147217396

      https://support.microsoft.com/en-us/kb/245115

    四、outlook版本库兼容问题

      因为可能不同用户安装的的office版本是不同的,所以可能他们使用的outlook版本库是不同的。不同的outlook版本有不同的类型库,对应如下:

    Outlook VersionType Library
    97 msoutl8.olb
    98 msoutl85.olb
    2000 msoutl9.olb
    2002 msoutl.olb
    2003 msoutl.olb

       可以参考以下链接:

      https://support.microsoft.com/en-us/kb/220600

       怎么才能使我们的程序兼容不同的outlook版本呢?有以下两个方案:

      1、使用early bind/MFC,使用要兼容的最早的版本的类库。因为early bind必须要选择一个类库来导入,而office版本是向下兼容的,所以如果我们最早要兼容到office 2003,就选择office 2003的版本的类型库。

      2、使用late binding。这应该是late binding最大的优点了,即它不需要指定outlook版本,它会自动根据用户当前安装的office版本选择要使用的类型库。

  • 相关阅读:
    谷粒商城网关服务建立(七)
    谷粒商城Nacos配置(六)
    谷粒商城Nacos配置(五)
    Nacos入门
    谷粒商城心得二
    谷粒商城的快速开发(四)
    谷粒商城的快速开发(三)
    谷粒商城心得一
    谷粒商城的环境搭建(二)
    PowerDesigner16安装&破解
  • 原文地址:https://www.cnblogs.com/stormpeach/p/4810018.html
Copyright © 2020-2023  润新知