最新消息:ww12345678 的部落格重装上线,希望大家继续支持。

利用定制Web服务将外部系统集成到D365F&O

网络文摘 William 2浏览 0评论

目的:

本文概述了利用网络服务在Dynamics 365财务与运营中创建销售订单的流程。它详细介绍了一个API,旨在促进外部系统生成销售订单,包括销售订单、销售线和响应的数据合同结构,以及服务内的验证和处理方法。

应用:

Dynamics 365 财务与运营

业务需求:

要求通过单一API调用,从外部系统创建D365 Finance中的销售订单。

解决方案:

许多使用 Dynamics 365 Finance 的企业依赖于不同的平台来生成销售订单——账单系统、客户关系管理(CRM)、电子商务平台或客户门户——并要求这些交易自动流入 D365 Finance,无需手动重新录入。

该解决方案直接在 D365 Finance 内部构建了定制的 Web 服务,能够接受来自任何外部系统的销售订单请求,并在整个创建流程中完成:头部数据、行项、付款条款、税款和销售确认——所有这些都在单一 API 调用中完成。

该服务在每一步都进行验证,在写入任何数据之前,拒绝缺少客户、无效商品或零值行的请求。错误会以描述性消息返回,以便调用系统能够干净利落地处理故障。成功请求后,D365 Finance会收到确认的销售订单,准备开具发票。

这种模式消除了手动订单录入,降低了系统间数据错误的风险,并为你的集成提供了可靠且可维护的基础,并随着交易量的增长而扩展。

邮差
邮差 WQFM9RL3E6

代码

网络请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/// <summary>
/// This is the data contract class for the web request for <c>ATLASBillingIntegrationService</c>.
/// </summary>
[DataContractAttribute]
class ATLASBillingIntegrationRequest
{
    private SelectableDataArea dataAreaId;
    private ATLASBillingIntegrationRequestSalesOrder salesOrderRequest;
    private List salesLinesRequest;
    /// <summary>
    /// Gets or sets the value of data contract parameter DataAreaId
    /// </summary>
    /// <param name = "_dataAreaId">
    /// The new value of data contract parameter DataAreaId
    /// </param>
    /// <returns>
    /// The current value of data contract parameter DataAreaId
    /// </returns>
    [DataMember("DataAreaId")]
    public SelectableDataArea parmDataAreaId(SelectableDataArea _dataAreaId = dataAreaId)
    {
        dataAreaId = _dataAreaId;
        return dataAreaId;
    }
    /// <summary>
    /// Gets or sets the value of data contract parameter SalesOrderRequest
    /// </summary>
    /// <param name = "_salesOrderRequest">
    /// The new value of data contract parameter SalesOrderRequest
    /// </param>
    /// <returns>
    /// The current value of data contract parameter SalesOrderRequest
    /// </returns>
    [DataMember("SalesOrderRequest")]
    public ATLASBillingIntegrationRequestSalesOrder parmSalesOrderRequest(ATLASBillingIntegrationRequestSalesOrder _salesOrderRequest = salesOrderRequest)
    {
        salesOrderRequest = _salesOrderRequest;
        return salesOrderRequest;
    }
    /// <summary>
    /// Gets or sets the value of data contract parameter SalesLinesRequest
    /// </summary>
    /// <param name = "_salesLinesRequest">
    /// The new value of data contract parameter SalesLinesRequest
    /// </param>
    /// <returns>
    /// The current value of data contract parameter SalesLinesRequest
    /// </returns>
    [
        DataMember("SalesLinesRequest"),
        DataCollection(Types::Class, classStr(ATLASBillingIntegrationRequestSalesLine)),
        AifCollectionTypeAttribute('_salesLinesRequest', Types::Class, classStr(ATLASBillingIntegrationRequestSalesLine)),
        AifCollectionTypeAttribute('return', Types::Class, classStr(ATLASBillingIntegrationRequestSalesLine))
    ]
    public List parmSalesLinesRequest(List _salesLinesRequest = salesLinesRequest)
    {
        salesLinesRequest = _salesLinesRequest;
        return salesLinesRequest;
    }
}
网络销售订单请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/// <summary>
/// This is the data contract class for the sales order request.
/// </summary>
[DataContractAttribute]
class ATLASBillingIntegrationRequestSalesOrder
{
    private CustAccount custAccount;
    private TaxAmountCur actualSalesTaxAmount;
    private PaymTermId paymTermId;
    private SalesFixedDueDate dueDate;
    private CustRef customerRef;
    private SalesOriginId salesOriginId;
    /// <summary>
    /// Gets or sets the value of data contract parameter CustAccount
    /// </summary>
    /// <param name = "_custAccount">
    /// The new value of data contract parameter CustAccount
    /// </param>
    /// <returns>
    /// The current value of data contract parameter CustAccount
    /// </returns>
    [DataMember("CustAccount")]
    public CustAccount parmCustAccount(CustAccount _custAccount = custAccount)
    {
        custAccount = _custAccount;
        return custAccount;
    }
    /// <summary>
    /// Gets or sets the value of data contract parameter ActualSalesTaxAmount
    /// </summary>
    /// <param name = "_actualSalesTaxAmount">
    /// The new value of data contract parameter ActualSalesTaxAmount
    /// </param>
    /// <returns>
    /// The current value of data contract parameter ActualSalesTaxAmount
    /// </returns>
    [DataMember("ActualSalesTaxAmount")]
    public TaxAmountCur parmActualSalesTaxAmount(TaxAmountCur _actualSalesTaxAmount = actualSalesTaxAmount)
    {
        actualSalesTaxAmount = _actualSalesTaxAmount;
        return actualSalesTaxAmount;
    }
    /// <summary>
    /// Gets or sets the value of data contract parameter PaymentTerms
    /// </summary>
    /// <param name = "_paymTermId">
    /// The new value of data contract parameter PaymentTerms
    /// </param>
    /// <returns>
    /// The current value of data contract parameter PaymentTerms
    /// </returns>
    [DataMember("PaymentTerms")]
    public PaymTermId parmPaymentTerms(PaymTermId _paymTermId = paymTermId)
    {
        paymTermId = _paymTermId;
        return paymTermId;
    }
    /// <summary>
    /// Gets or sets the value of data contract parameter DueDate
    /// </summary>
    /// <param name = "_dueDate">
    /// The new value of data contract parameter DueDate
    /// </param>
    /// <returns>
    /// The current value of data contract parameter DueDate
    /// </returns>
    [DataMember("DueDate")]
    public SalesFixedDueDate parmDueDate(SalesFixedDueDate _dueDate = dueDate)
    {
        dueDate = _dueDate;
        return dueDate;
    }
    /// <summary>
    /// Gets or sets the value of data contract parameter SalesOrigin
    /// </summary>
    /// <param name = "_salesOrigin">
    /// The new value of data contract parameter SalesOrigin
    /// </param>
    /// <returns>
    /// The current value of data contract parameter SalesOrigin
    /// </returns>
    [DataMember("SalesOrigin")]
    public SalesOriginId parmSalesOrigin(SalesOriginId _salesOriginId = salesOriginId)
    {
        salesOriginId = _salesOriginId;
        return salesOriginId;
    }
    /// <summary>
    /// Gets or sets the value of data contract parameter Description
    /// </summary>
    /// <param name = "_customerRef">
    /// The new value of data contract parameter Description
    /// </param>
    /// <returns>
    /// The current value of data contract parameter Description
    /// </returns>
    [DataMember("Description")]
    public CustRef parmCustomerRef(CustRef _customerRef = customerRef)
    {
        customerRef = _customerRef;
        return customerRef;
    }
}
网络销售订单线索请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/// <summary>
/// This is the data contract class for the sales order lines request.
/// </summary>
[DataContractAttribute]
class ATLASBillingIntegrationRequestSalesLine
{
    private ItemId itemId;
    private LineAmount lineAmount;
    private TaxAmount taxAmount;
    private ItemFreeTxt itemName;
    private SalesQty salesQty;
    /// <summary>
    /// Gets or sets the value of data contract parameter ItemId
    /// </summary>
    /// <param name = "_itemId">
    /// The new value of data contract parameter ItemId
    /// </param>
    /// <returns>
    /// The current value of data contract parameter ItemId
    /// </returns>
    [DataMember("ItemId")]
    public ItemId parmItemId(ItemId _itemId = itemId)
    {
        itemId = _itemId;
        return itemId;
    }
    /// <summary>
    /// Gets or sets the value of data contract parameter LineAmount
    /// </summary>
    /// <param name = "_lineAmount">
    /// The new value of data contract parameter LineAmount
    /// </param>
    /// <returns>
    /// The current value of data contract parameter LineAmount
    /// </returns>
    [DataMember("LineAmount")]
    public LineAmount parmLineAmount(LineAmount _lineAmount = lineAmount)
    {
        lineAmount = _lineAmount;
        return lineAmount;
    }
    /// <summary>
    /// Gets or sets the value of data contract parameter TaxAmount
    /// </summary>
    /// <param name = "_taxAmount">
    /// The new value of data contract parameter TaxAmount
    /// </param>
    /// <returns>
    /// The current value of data contract parameter TaxAmount
    /// </returns>
    [DataMember("TaxAmount")]
    public TaxAmount parmTaxAmount(TaxAmount _taxAmount = taxAmount)
    {
        taxAmount = _taxAmount;
        return taxAmount;
    }
    /// <summary>
    /// Gets or sets the value of data contract parameter ItemName
    /// </summary>
    /// <param name = "_itemName">
    /// The new value of data contract parameter ItemName
    /// </param>
    /// <returns>
    /// The current value of data contract parameter ItemName
    /// </returns>
    [DataMember("ItemName")]
    public ItemFreeTxt parmItemName(ItemFreeTxt _itemName = itemName)
    {
        itemName = _itemName;
        return itemName;
    }
    /// <summary>
    /// Gets or sets the value of data contract parameter SalesQty
    /// </summary>
    /// <param name = "_salesQty">
    /// The new value of data contract parameter SalesQty
    /// </param>
    /// <returns>
    /// The current value of data contract parameter SalesQty
    /// </returns>
    [DataMember("Quantity")]
    public SalesQty parmSalesQty(SalesQty _salesQty = salesQty)
    {
        salesQty = _salesQty;
        return salesQty;
    }
}
网络响应
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/// <summary>
/// This is the data contract class for the web response for <c>ATLASBillingIntegrationService</c>.
/// </summary>
[DataContractAttribute]
class ATLASBillingIntegrationResponse
{
    private boolean success;
    private str errorMessage;
    private str debugMessage;
    /// <summary>
    /// Gets or sets the value of data contract parameter Success
    /// </summary>
    /// <param name = "_success">
    /// The new value of data contract parameter Success
    /// </param>
    /// <returns>
    /// The current value of data contract parameter Success
    /// </returns>
    [DataMember("Success")]
    public boolean parmSuccess(boolean _success = success)
    {
        success = _success;
        return success;
    }
    /// <summary>
    /// Gets or sets the value of data contract parameter ErrorMessage
    /// </summary>
    /// <param name = "_errorMessage">
    /// The new value of data contract parameter ErrorMessage
    /// </param>
    /// <returns>
    /// The current value of data contract parameter ErrorMessage
    /// </returns>
    [DataMember("ErrorMessage")]
    public str parmErrorMessage(str _errorMessage = errorMessage)
    {
        errorMessage = _errorMessage;
        return errorMessage;
    }
    /// <summary>
    /// Gets or sets the value of data contract parameter DebugMessage
    /// </summary>
    /// <param name = "_debugMessage">
    /// The new value of data contract parameter DebugMessage
    /// </param>
    /// <returns>
    /// The current value of data contract parameter DebugMessage
    /// </returns>
    [DataMember("DebugMessage")]
    public str parmDebugMessage(str _debugMessage = debugMessage)
    {
        debugMessage = _debugMessage;
        return debugMessage;
    }
}
网络服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
/// <summary>
/// This is the service class for <c>ATLASBillingIntegrationService</c>.
/// </summary>
class ATLASBillingIntegrationService
{
    #OCCRetryCount
    #define.SalesOrigin('ExtBillingSystem')
    /// <summary>
    /// Creates sales order and sales invoice
    /// </summary>
    /// <param name = "_request">
    /// Web request
    /// </param>
    /// <returns>
    /// Web response
    /// </returns>
    public ATLASBillingIntegrationResponse run(ATLASBillingIntegrationRequest _request)
    {
        ATLASBillingIntegrationResponse response;
        ATLASBillingIntegrationRequestSalesOrder salesOrderRequest;
        List salesLinesRequest;
        SelectableDataArea dataAreaId;
        SalesId salesId;
        InvoiceDate invoiceDate;
        str infologMessages;
        dataAreaId = _request.parmDataAreaId();
        invoiceDate = today();
        salesOrderRequest = _request.parmSalesOrderRequest();
        salesLinesRequest = _request.parmSalesLinesRequest();
        response = new ATLASBillingIntegrationResponse();
        this.validateWebRequest(_request);
        changecompany(dataAreaId)
        {
            try
            {
                ttsbegin;
                salesId = this.createSalesOrder(salesOrderRequest);
                this.createSalesOrderLines(salesId, salesLinesRequest);
                this.postSalesConfirmation(salesId, invoiceDate);
                ttscommit;
                response.parmSuccess(true);
                if (response.parmSuccess())
                {
                    response.parmDebugMessage(strFmt("@ATLAS:SalesOrderCreated", salesId, dataAreaId));
                }
                else
                {
                    response.parmErrorMessage(strFmt("@ATLAS:ErrorOccurred"));
                }
            }
            catch (Exception::Deadlock)
            {
                retry;
            }
            catch (Exception::UpdateConflict)
            {
                if (appl.ttsLevel() == 0)
                {
                    if (xSession::currentRetryCount() >= #RetryNum)
                    {
                        throw Exception::UpdateConflictNotRecovered;
                    }
                    else
                    {
                        retry;
                    }
                }
                else
                {
                    throw Exception::UpdateConflict;
                }
            }
            catch (Exception::Error)
            {
                infologMessages = this.getInfologMessages();
                response.parmErrorMessage(strFmt("%1", infologMessages));
            }
        }
        return response;
    }
    /// <summary>
    /// Gets infolog messages
    /// </summary>
    /// <returns>
    /// Infolog messages string value
    /// </returns>
    private str getInfologMessages()
    {
        SysInfologEnumerator enumerator;
        ListEnumerator le;
        List messageList;
        str allMessages;
        enumerator = SysInfologEnumerator::newData(infolog.infologData());
        messageList = new List(Types::String);
        allMessages = "";
        while (enumerator.moveNext())
        {
            messageList.addEnd(enumerator.currentMessage());
        }
        infolog.clear();
        le = messageList.getEnumerator();
        while (le.moveNext())
        {
            if (allMessages == "")
            {
                allMessages = le.current();
            }
            else
            {
                allMessages += " " + le.current();
            }
        }
        return allMessages;
    }
    /// <summary>
    /// Validates web request
    /// </summary>
    /// <param name = "_request">
    /// Web request
    /// </param>
    private void validateWebRequest(ATLASBillingIntegrationRequest _request)
    {
        ATLASBillingIntegrationRequestSalesOrder salesOrderRequest;
        ATLASBillingIntegrationRequestSalesLine salesLineRequest;
        ListIterator listIterator;
        List salesLinesRequest;
        SelectableDataArea dataAreaId;
        CustAccount custAccount;
        PaymTermId paymTermId;
        ItemId itemId;
        LineAmount lineAmount;
        SalesOriginId salesOriginId;
        SalesQty salesQty;
        dataAreaId = _request.parmDataAreaId();
        salesOrderRequest = _request.parmSalesOrderRequest();
        salesLinesRequest = _request.parmSalesLinesRequest();
        custAccount = salesOrderRequest.parmCustAccount();
        paymTermId = salesOrderRequest.parmPaymentTerms();
        salesOriginId = salesOrderRequest.parmSalesOrigin();
        if (!CompanyInfo::findDataArea(dataAreaId))
        {
            throw error(strFmt("@ATLAS:CompanyDoesNotExist", dataAreaId));
        }
        changecompany(dataAreaId)
        {
            if (!CustTable::exist(custAccount))
            {
                throw error(strFmt("@ATLAS:CustomerDoesNotExist", custAccount));
            }
            if (!PaymTerm::exist(paymTermId))
            {
                throw error(strFmt("@ATLAS:PaymentTermDoesNotExist", paymTermId));
            }
            if (!SalesOrigin::exist(salesOriginId))
            {
                throw error(strFmt("@ATLAS:SalesOriginDoesNotExist", salesOriginId));
            }
            if (salesLinesRequest)
            {
                listIterator = new ListIterator(salesLinesRequest);
                while (listIterator.more())
                {
                    salesLineRequest = listIterator.value();
                    itemId = salesLineRequest.parmItemId();
                    lineAmount = salesLineRequest.parmLineAmount();
                    salesQty = salesLineRequest.parmSalesQty();
                    if (!InventTable::exist(itemId))
                    {
                        throw error(strFmt("@ATLAS:ItemDoesNotExist", itemId));
                    }
                    if (lineAmount == 0)
                    {
                        throw error(strFmt("@ATLAS:LineAmountNotProvided", salesLineRequest.parmItemId()));
                    }
                    if (salesQty == 0)
                    {
                        throw error(strFmt("@ATLAS:QuantityNotProvided", salesLineRequest.parmItemId()));
                    }
                    listIterator.next();
                }
            }
        }
    }
    /// <summary>
    /// Creates sales order header
    /// </summary>
    /// <param name = "_salesOrderRequest">
    /// Sales order details
    /// </param>
    /// <returns>
    /// Sales order number
    /// </returns>
    private SalesId createSalesOrder(ATLASBillingIntegrationRequestSalesOrder _salesOrderRequest)
    {
        CustAccount custAccount;
        SalesTable salesTable;
        PaymTermId paymTermId;
        SalesOriginId salesOriginId;
        CustRef customerRef;
        NumberSeq numberSeq;
        custAccount = _salesOrderRequest.parmCustAccount();
        salesOriginId = _salesOrderRequest.parmSalesOrigin();
        customerRef = _salesOrderRequest.parmCustomerRef();
        paymTermId = PaymTerm::find(_salesOrderRequest.parmPaymentTerms()).PaymTermId;
        numberSeq = NumberSeq::newGetNum(SalesParameters::numRefSalesId());
        numberSeq.used();
        salesTable.SalesId = numberSeq.num();
        salesTable.initValue();
        salesTable.CustAccount = custAccount;
        salesTable.initFromCustTable();
        salesTable.SalesOriginId = SalesOrigin::find(salesOriginId).OriginId;
        salesTable.CustomerRef = customerRef;
        salesTable.Payment = paymTermId;
        salesTable.FixedDueDate = _salesOrderRequest.parmDueDate();
        if (!salesTable.validateWrite())
        {
            throw Exception::Error;
        }
        salesTable.insert();
        return salesTable.SalesId;
    }
    /// <summary>
    /// Creates sales order lines
    /// </summary>
    /// <param name = "_salesId">
    /// Sales order number
    /// </param>
    /// <param name = "_salesLinesRequest">
    /// List of sales order lines
    /// </param>
    private void createSalesOrderLines(SalesId _salesId, List _salesLinesRequest)
    {
        ATLASBillingIntegrationRequestSalesLine salesLineRequest;
        ListIterator listIterator;
        SalesTable salesTable;
        SalesLine salesLine;
        ItemId itemId;
        SalesQty salesQty;
        LineAmount lineAmount;
        TaxAmount taxAmount;
        ItemFreeTxt itemName;
        if (_salesLinesRequest)
        {
            listIterator = new ListIterator(_salesLinesRequest);
            while (listIterator.more())
            {
                salesLineRequest = listIterator.value();
                
                salesTable = SalesTable::find(_salesId);
                itemId = salesLineRequest.parmItemId();
                lineAmount = salesLineRequest.parmLineAmount();
                taxAmount = salesLineRequest.parmTaxAmount();
                itemName = salesLineRequest.parmItemName();
                salesQty = salesLineRequest.parmSalesQty();
                salesLine.clear();
                salesLine.initFromSalesTable(salesTable);
                salesLine.SalesId = salesTable.SalesId;
                salesLine.ItemId = itemId;
                salesLine.SalesQty = salesQty;
                salesLine.createLine(true, true, true, true, true, true);
                salesLine.SalesPrice = lineAmount;
                salesLine.modifiedField(fieldNum(SalesLine, SalesPrice));
                salesLine.ATLASTaxAmount = taxAmount;
                salesLine.Name = itemName;
                salesLine.update();
                listIterator.next();
            }
        }
    }
    /// <summary>
    /// Posts sales order confirmation
    /// </summary>
    /// <param name = "_salesId">
    /// Sales order number
    /// </param>
    private void postSalesConfirmation(SalesId _salesId, TransDate _transDate)
    {
        SalesFormLetter salesFormLetter;
        SalesTable salesTable;
        salesTable = SalesTable::find(_salesId);
        salesFormLetter = SalesFormLetter::construct(DocumentStatus::Confirmation);
        salesFormLetter.update(salesTable, _transDate);
    }
}

需要为您的业务打造这个设备吗?

如果你的团队在系统间手动管理销售订单创建,或者当前的集成不够可靠,无法大规模信任,我们可以提供帮助。

Atlas Dynamics 专注于打造定制的 D365 Finance 集成,这些集成既准备好生产,经过妥善验证,并设计得持久。无论您需要将计费平台、客户关系管理系统(CRM)、电子商务系统或内部门户连接到D365 Finance,我们的团队都能从范围界定、开发到测试和上线支持,提供端到端的服务。

转载请注明:ww12345678 的部落格 | AX Helper » 利用定制Web服务将外部系统集成到D365F&O

发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址