深入Atlas系列:Web Sevices Access in Atlas示例(1) - 特别的访问方式

  注意:部分内容已经过期,请结合《深入Atlas系列:Web Sevices Access in Atlas(7) - RTM中的客户端支持》阅读此文。

在《深入Atlas系列:Web Sevices Access in Atlas(1) - 客户端支持》里我们分析了Atlas客户端以AJAX方式访问Web Services方法所使用的基础代码,那就是Sys.Net.ServiceMethod,它提供了对于Web Service方法访问的封装。有了它,我们可以很方便地访问Web Services方法。但是在Atlas中,我们有更加方便的访问方式,在这篇文章里,我们就来讨论一下这些方法。


一、使用Sys.Net.ServiceMethod.invoke静态方法访问Web Services

如果阅读了Atlas代码之后,可以发现在客户端一些需要访问Web Services方法的类,例如AutoCompleteBehavior,它们在访问Web Services方法的时候并没有直接使用Sys.Net.ServiceMethod,而是调用了Sys.Net.ServiceMethod类的静态方法invoke。在以后的文章中我会分析Atlas客户端对Web Service方法提供代理的实现,届时也能发现,事实上在代理内部,使用的也是该静态方法。这个静态方法相当简单,不过我们还是来分析一下吧。代码如下:
29174333_Pu8c.gif 29174333_k7JL.gif Sys.Net.ServiceMethod.invoke静态方法分析
 1 // methodURL:Web Services文件URL
 2 // methodName:Web Services方法名
 3 // appUrl:Web应用程序路径
 4 Sys.Net.ServiceMethod.invoke = function(methodURL, methodName, appUrl) {
 5 
 6     // 使用参数构造一个Sys.Net.ServiceMethod对象
 7     var method = new Sys.Net.ServiceMethod(methodURL, methodName, appUrl);
 8 
 9     // 准备使用上面method对象的参数数组
10     var callMethodArgs = new Array();
11 
12     // 从第4个参数开始,依次放入新的参数数组
13     for (var i = 3; i < arguments.length; i++)
14     {
15         callMethodArgs[i-3= arguments[i];
16     }
17 
18     // 使用准备好的参数数组调用method对象的invoke方法
19     return method.invoke.apply(method, callMethodArgs);
20 }

代码如想象中的简单,只是在静态方法内部构造一个Sys.Net.ServiceMethod类的对象,并调用它的invoke方法。由于Sys.Net.ServiceMethod的invoke方法提供了“神奇”的“函数重载”(详细信息请见《 深入Atlas系列:Web Sevices Access in Atlas(1) - 客户端支持》的分析),因此Sys.Net.ServiceMethod.invoke静态方法也能通过两种方式调用,如下:

第一种是:
29174333_Pu8c.gif 29174333_k7JL.gif Sys.Net.ServiceMethod.invoke静态方法分析
 1 Sys.Net.ServiceMethod.invoke(
 2     methodURL,
 3     methodName,
 4     appurl,
 5     {
 6         param1 : value1,
 7         param2 : value2,
 8         ……
 9     },
10     {
11         onMethodComplete : ……,
12         onMethodTimeout : ……,
13         onMethodError : ……,
14         onMethodAborted : ……,
15         userContext : ……,
16         timeoutInterval : ……,
17         priority : ……,
18         useGetMethod : ……
19     });

第二种是:
29174333_Pu8c.gif 29174333_k7JL.gif Sys.Net.ServiceMethod.invoke静态方法分析
 1 Sys.Net.ServiceMethod.invoke(
 2     methodURL,
 3     methodName,
 4     appurl,
 5     {
 6         param1 : value1,
 7         param2 : value2,
 8         ……
 9     },
10     onMethodComplete,
11     onMethodTimeout,
12     onMethodError,
13     onMethodAborted,
14     userContext,
15     timeoutInterval,
16     priority,
17     useGetMethod);

于是,以后就能使用这种比较简洁的方式访问Web Services方法了。


二、使用Declarative Syntax访问Web Services方法

深入代码可以发现各种有效的使用方式,尤其在现在这样文档极其匮乏的时期。事实上,对于使用Declarative Syntax访问Web Services的描述才是这篇文章的重点。仔细Atlas代码之后,可以发现在这样一个类:
1  Sys.Net.ServiceMethodRequest  =   function () {
2      ……
3  }
4  Sys.Net.ServiceMethodRequest.registerClass('Sys.Net.ServiceMethodRequest', Sys.Component);
5  Sys.TypeDescriptor.addType('script', 'serviceMethod', Sys.Net.ServiceMethodRequest);

在我之前的文章《 使用Atlas创建自己的Client Control》里简单分析了一点:使用Sys.TypeDescriptor.addType方法能够为一个类提供Declarative Syntax的支持。关于这一点的具体实现方式已经超出了现在这篇文章的讨论范围,但是我会在“深入Atlas系列”的后续文章里从实现角度具体分析Atlas对于Xml Scripts的解析方式,敬请留意。:)

对于支持Declarative Syntax的类,其最重要的方法应该就是this.getDescriptor了(确切的说,这个是Sys.ITypeDescriptorProvider接口的方法。对于没有实现该接口的类,Atlas在识别其成员时使用的是别的方式。不过由于Sys.Component实现了Sys.ITypeDescriptorProvider接口,因此我们如果继承了Sys.Component,只要重载getDescriptor方法就可以了)。它提供了对于类成员的描述,Atlas在进行操作时会频繁使用这些信息,因此该方法非常重要。我们来看一下它在Sys.Net.ServiceMethodRequest中的实现:
29174333_Pu8c.gif 29174333_k7JL.gif getDescriptor方法实现
 1 this.getDescriptor = function() {
 2     var td = Sys.Net.ServiceMethodRequest.callBaseMethod(this, 'getDescriptor');
 3     
 4     // --- 属性 ---
 5     // Web Services的URL
 6     td.addProperty('url', String);
 7     // Web应用程序URL
 8     td.addProperty('appUrl', String);
 9     // Web Services方法名
10     td.addProperty('methodName', String);
11     // 参数字典,将在后面具体讨论,只度
12     td.addProperty('parameters', Object, true);
13     // response对象,在调用以后可以获得,只读
14     td.addProperty('response', Sys.Net.WebRequestExecutor, true);
15     // 调用后的结果,制度
16     td.addProperty('result', Object, true);
17     // 超时间隔
18     td.addProperty('timeoutInterval', Number);
19     // 优先级,按理应该是Sys.Net.WebRequestPriority枚举类型
20     td.addProperty('priority', Number);
21 
22     // --- 方法 ---
23     // invoke方法,调用该Web Service方法
24     td.addMethod('invoke');
25     // abort,取消该Web Service方法调用
26     td.addMethod('abort');
27 
28     // --- 事件 ---
29     // completed事件,在调用完成后触发
30     td.addEvent('completed', true);
31     // timeout事件,在调用超时后触发
32     td.addEvent('timeout', true);
33     // error事件,在调用出错时触发
34     td.addEvent('error', true);
35     // aborted事件,在调用被取消时触发
36     td.addEvent('aborted', true);
37 
38     return td;
39 }
40 Sys.Net.ServiceMethodRequest.registerBaseMethod(this, 'getDescriptor');

以上就是Sys.Net.ServiceMethodRequest类的成员描述,应该说还是相当直观的。

接着开始分析Sys.Net.ServiceMethodRequest类的主要成员,一些属性的get/set方法就忽略了。其实整个类唯一作用较大的方法就是this.invoke。代码如下:
29174333_Pu8c.gif 29174333_k7JL.gif this.invoke方法分析
 1 this.invoke = function(userContext) {
 2     // 如果一个Request正在生效,则退出
 3     if (_request != null) {
 4         return false;
 5     }
 6 
 7     // 构造一个Sys.Net.ServiceMethod对象
 8     var serviceMethod = new Sys.Net.ServiceMethod(_url, _methodName, _appUrl);
 9     // 将Web Services方法参数字典和每个回调函数作为参数传入invoke方法
10     _request = serviceMethod.invoke(_parameters, onMethodComplete, onMethodTimeout, 
11         onMethodError, onMethodAborted, this ,
12         _timeoutInterval, _priority);
13 
14     function onMethodComplete(result, response, target ) {
15         // 调用完成,将_request设为null
16         _request = null;
17         _userContext = userContext;
18         _response = response;
19         _result = result;
20         // 触发completed事件
21         target.completed.invoke(target, Sys.EventArgs.Empty);
22     }
23 
24     function onMethodError(result, response, target ) {
25         // 调用完成,将_request设为null
26         _request = null;
27         _userContext = userContext;
28         _response = response;
29         _result = result;
30         // 触发error事件
31         target.error.invoke(target, Sys.EventArgs.Empty);
32     }
33 
34     function onMethodTimeout(request, target ) {
35         // 调用完成,将_request设为null
36         _request = null;
37         _userContext = userContext;
38         // 触发timeout事件
39         target.timeout.invoke(request, Sys.EventArgs.Empty);
40     }
41         
42     function onMethodAborted(request, target ) {
43         // 调用完成,将_request设为null
44         _request = null;
45         _userContext = userContext;
46         // 出发abort事件
47         target.aborted.invoke(request, Sys.EventArgs.Empty);
48     }
49         
50     return true;
51 }

整个Sys.Net.ServiceMethodRequest类的代码就分析完了!看得出来它只是对Sys.Net.ServiceMethod类进行了一个封装,但是就是这种简单的封装就能提供Declarative Syntax支持!原因就在于Atlas的代码已经对于解析Xml Scripts和识别一个类做了非常多的工作,我们当然就省事了。

不过在这个类中,有一个非常有意思的成员,那就是parameters属性。它是个只读属性,返回一个对象作为参数字典,我们能够对其以key - value的方式进行设置。事实上我们不是第一次遇到这个东西了。在最常用的Action之一:Sys.InvokeMethodAction就有该参数,该参数允许这样使用:
< parameters  param1 ="value1"  param2 ="value2"  ... />

很自然,我们的parameters属性也能如此使用。Atlas在解析Xml时对于这样的情况,会将Xml属性名和值以key - value的形式存放在该parameters对象中。

但是,这远远不够!我们为什么要在getDescripter方法中给出该属性的定义?目的不是为了给我们的代码调用(只要存在我们代码就能访问,根本无须通过这个方法获得类成员),而是为了让Declarative Syntax识别!有了类成员的描述,我们就能使用各种Action,还有Atlas的特色之一:Binding。

对了,大家应该也已经想到了,我们可以将parameters属性和“别的什么”绑定起来啊。但是先别高兴太早,因为parameters是只读属性,按照普通的方法无法将一个值赋于该属性。但是Atlas想到了这一点,它的Binding能够处理这种情况。在后面的例子中可以看到,我们可以如此使用Binding。
< bindings >
    
< binding  dataContext ="txtName"  dataPath ="text"  property ="parameters"  propertyKey ="name"   />
    
< binding  dataContext ="txtAge"  dataPath ="text"  property ="parameters"  propertyKey ="age"   />
</ bindings >

这里使用到了目前文档中不曾记载的Binding使用方式,它用到了propertyKey。Atlas在处理Binding时,如果发现用户提供了propertyKey,则首先使用property的get方法获得这个对象的属性的值,然后再把propertyKey的值作为key,以key - value的方式设置该属性。这个做法简直就是为了parameters这样的属性量身定制的!

Sys.Net.ServiceMethodRequest类的分析到这里应该就足够了。接下来,我们通过一个例子来具体看一下该如何使用Declarative Syntax调用Web Services方法。


三、使用Declarative Syntax访问Web Services方法范例

首先,我们定义所需要使用的Web Services方法和类:
29174333_Pu8c.gif 29174333_k7JL.gif EmployeeService.asmx文件代码
 1 [WebService(Namespace = "http://tempuri.org/")]
 2 [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
 3 public class SampleService  : System.Web.Services.WebService {
 4 
 5     [WebMethod]
 6     public List<Employee> AddEmployee(string name, int age)
 7     {
 8         Employee.AllEmployees.Add(new Employee(name, age));
 9         return Employee.AllEmployees;
10     }
11 }
12 
13 public class Employee
14 {
15     public static List<Employee> AllEmployees = new List<Employee>();
16     
17     public string Name;
18 
19     public int Age;
20 
21     public Employee() { }
22     
23     public Employee(string name, int age)
24     {
25         this.Name = name;
26         this.Age = age;
27     }
28 }

每次调用AddEmployee方法会在List中增加一个Employee对象,并输出整个Employee对象。

接下来是HTML:
29174333_Pu8c.gif 29174333_k7JL.gif HTML代码
 1 <atlas:ScriptManager runat="server" ID="ScriptManager1" />
 2         
 3 Name: <input type="text" id="txtName" /><br />
 4 Age: <input type="text" id="txtAge" /><br />
 5 <input type="button" value="invoke" id="btnInvoke" />
 6 <hr />
 7         
 8 <div id="listView"></div>
 9         
10 <!-- Layout Elements -->
11 <div style="display: none;">
12     <!-- Layout Template -->
13     <table id="layoutTemplate" border="1" cellpadding="0" cellspacing="0">
14         <thead>
15             <tr>
16                 <td>Name</td>
17                 <td>Age</td>
18             </tr>
19         </thead>
20         <!-- Repeat Template -->
21         <tbody id="itemTemplateParent">
22             <!-- Repeat Item Template -->
23             <tr id="itemTemplate">
24                 <td><span id="lblName" /></td>
25                 <td><span id="lblAge" /></td>
26             </tr>
27         </tbody>
28     </table>
29     <!-- Empty Template -->
30     <div id="emptyTemplate">
31         No Data
32     </div>
33 </div>

这个范例的目的是在两个文本框(txtName和txtAge)里填写一个Employee的姓名和年龄,然后点击按钮btnInvoke之后会调用Web Services方法增加一个Employee并得到一个Employee列表,然后显示在页面上。在这里,我会使用Atlas中的Sys.UI.Data.ListView控件来显示Employee列表。关于该控件的使用方式,可以参考Dflying兄的文章《 使用ASP.NET Atlas ListView控件显示列表数据》。

然后就是最重要的Atlas Xml Scripts了:
29174333_Pu8c.gif 29174333_k7JL.gif Atlas Xml Scripts
 1 <script type="text/xml-script">
 2     <page xmlns:jeffz="http://www.jeffzlive.net">
 3         <components>
 4             <button id="btnInvoke">
 5                 <click>
 6                     <invokeMethod target="employeeService" method="invoke" />
 7                 </click>
 8             </button>
 9             
10             <textBox id="txtName" />
11             <textBox id="txtAge" />
12             
13             <serviceMethod id="employeeService" url="EmployeeService.asmx" methodName="AddEmployee" completed="onComplete">
14                 <bindings>
15                     <binding dataContext="txtName" dataPath="text" property="parameters" propertyKey="name" />
16                     <binding dataContext="txtAge" dataPath="text" property="parameters" propertyKey="age" />
17                 </bindings>
18             </serviceMethod>
19             
20             <listView id="listView" itemTemplateParentElementId="itemTemplateParent">
21                 <layoutTemplate>
22                     <template layoutElement="layoutTemplate" />
23                 </layoutTemplate>
24                 <itemTemplate>
25                     <template layoutElement="itemTemplate">
26                         <label id="lblName">
27                             <bindings>
28                                 <binding dataPath="Name" property="text" />    
29                             </bindings>
30                         </label>
31                         <label id="lblAge">
32                             <bindings>
33                                 <binding dataPath="Age" property="text" />
34                             </bindings>
35                         </label>                    
36                     </template>
37                 </itemTemplate>
38                 <emptyTemplate>
39                     <template layoutElement="emptyTemplate"/>
40                 </emptyTemplate>
41             </listView>
42         </components>
43     </page>
44 </script>

我们关注一下最重要的<serviceMethod />使用吧,正如之前所提到的,我将txtName的值与name参数绑定起来,并且将txtAge的值与age参数绑定起来,就是这么简单。

嗯,先不急着运行,是不是看出什么问题来了?对,我们为什么没有将employeeService的result属性和listView的data属性绑定起来的呢?否则我们如何获得数据呢?其实我也想,这可以说是Sys.Net.ServiceMethodRequest的一个Bug:它在result更新是不会调用this.raisePropertyChanged方法!这样Binding怎么可能收到result更新的信息呢?对于这点我也相当无语。没有办法我们只能响应completed事件,让它调用onComplete这个javascript方法了。onComplete方法代码如下:
29174333_Pu8c.gif 29174333_k7JL.gif onComplete方法
1 <script type="text/javascript">
2     function onComplete(sender, args)
3     {
4         $("listView").control.set_data(sender.get_result());
5     }
6 </script>

代码非常简单,就这样起效果了。我们来看一下使用吧:

首先打开页面,会看到显示为No Data:
29174333_p9bR.jpg

在文本框内输入信息并点击按钮,则可以添加一个Employee,反复多次则添加多个:
29174333_MTXV.jpg



四、开发自己的Componet,完全使用Declarative Syntax访问Web Services方法

必须借助于Javascript才能完成任务,是不是总是觉得心理有点别扭?至少我是这样的。而且除此之外,Sys.Net.ServiceMethodRequest还有一个不合理的地方:它的Priority属性类型是Number,在写Xml的时候就必须把数字赋予该属性。因此,我们来修改一下它的代码,开发一下一个更好的ServiceMethodRequest吧。

本想继承Sys.Net.ServiceMethodRequest并重载invoke函数可是令人惊讶的是,我们无法这样做,因为Sys.Net.ServiceMethodRequest没有调用registerBaseMethod来注册invoke函数。虽然我们依旧可以写一个this.invoke = function() { ... }也可以正确运行,但是这个不是OO的Good Practise,因此我还是完整的写了一遍代码。

由于大部分代码和Sys.Net.ServiceMethodRequest相同,那么我就给出一小部分不同的代码吧。
29174333_Pu8c.gif 29174333_k7JL.gif Jeffz.Net.ServiceMethodRequest部分代码
 1 Type.registerNamespace('Jeffz.Net');
 2 
 3 Jeffz.Net.ServiceMethodRequest = function() {
 4     Jeffz.Net.ServiceMethodRequest.initializeBase(this);
 5     
 6     ……
 7     
 8     var _onCompleteHander = null;
 9     var _onErrorHander = null;
10 
11     ……
12 
13     this.get_parameters = function() {
14         if (_parameters == null) {
15             _parameters = Sys.Component.createCollection(this);
16         }
17         return _parameters;
18     }
19 
20     ……
21 
22     this.invoke = function(userContext) {
23         ……
24         
25         var params = new Object();
26         for (var i = 0; i < _parameters.length; i++)
27         {
28             params[_parameters[i].get_name()] = _parameters[i].get_value()
29         }
30 
31         if (_onCompleteHander == null)
32         {
33             _onCompleteHander = Function.createDelegate(this, onMethodComplete);
34         }
35         
36         if (_onErrorHander == null)
37         {
38             _onErrorHander = Function.createDelegate(this, onMethodError);
39         }
40             
41         var serviceMethod = new Sys.Net.ServiceMethod(_url, _methodName, _appUrl);
42         _request = serviceMethod.invoke(params, _onCompleteHander, onMethodTimeout, 
43             _onCompleteHander, onMethodAborted, this,
44             _timeoutInterval, _priority);
45             
46         
47         function onMethodComplete(result, response, target) {
48             ……
49             this.raisePropertyChanged("result");
50             ……
51         }
52 
53         function onMethodError(result, response, target) {
54             ……
55             this.raisePropertyChanged("result");
56             ……
57         }
58         
59         ……
60     }
61     Jeffz.Net.ServiceMethodRequest.registerBaseMethod(this, 'invoke');
62 
63     ……    
64 
65     this.getDescriptor = function() {
66         var td = Jeffz.Net.ServiceMethodRequest.callBaseMethod(this, 'getDescriptor');
67         
68         ……
69 
70         td.addProperty('parameters', Array, true);
71 
72         ……
73 
74         td.addProperty('priority', Sys.Net.WebRequestPriority);
75 
76         ……
77     }
78     Jeffz.Net.ServiceMethodRequest.registerBaseMethod(this, 'getDescriptor');
79     
80     
81     this.dispose = function() {
82         _parameters.dispose();
83         _parameters = null;
84         
85         _onCompleteHander = null;
86         _onErrorHander = null;
87         
88         Jeffz.Net.ServiceMethodRequest.callBaseMethod(this, 'dispose');
89     }
90     Jeffz.Net.ServiceMethodRequest.registerBaseMethod(this, 'dispose');
91 }
92 Jeffz.Net.ServiceMethodRequest.registerClass('Jeffz.Net.ServiceMethodRequest', Sys.Component);
93 Sys.TypeDescriptor.addType('jeffz', 'serviceMethod', Jeffz.Net.ServiceMethodRequest);

可以看到,我在onMethodComplete和onMethodError方法里都加了this.raisePropertyChanged("result")的调用。于是Binding就能收到result改变的消息,重新去获得它的值了。为了让“this”引用在上面两个回调函数内被正确的指向,因此使用了Function.createDelegate方法。在我之前的文章《 使用Atlas创建自己的Client Control》也提到过这一点。另外,我还将priority属性改成了Sys.Net.WebRequestPriority枚举类型,这样我们就能使用"High"和"Normal"等字样了。

等等,你还发现了什么?我将parameters属性的类型改成了Array,它的get方法也变了。这是我的一个尝试,将parameters属性变成了一个Jeffz.Net.Parameter的Collection。属性类型方面的细节涉及到对于Atlas Xml Scripts的解析,因此现在不做讨论。Jeffz.Net.Parameter很简单,代码如下:
29174333_Pu8c.gif 29174333_k7JL.gif Jeffz.Net.Parameter代码
 1 Jeffz.Net.Parameter = function()
 2 {
 3     Jeffz.Net.Parameter.initializeBase(this);
 4     
 5     var _name = null;
 6     var _value = null;
 7     
 8     this.get_name = function()
 9     {
10         return _name;
11     }
12     
13     this.set_name = function(value)
14     {
15         _name = value;
16     }
17     
18     this.get_value = function()
19     {
20         return _value;
21     }
22     
23     this.set_value = function(value)
24     {
25         _value = value;
26     }
27     
28     this.setOwner = function(owner) {
29         this._owner = owner;
30     }
31     
32     this.getDescriptor = function() {
33         var td = Jeffz.Net.Parameter.callBaseMethod(this, 'getDescriptor');
34         
35         td.addProperty('name', String);
36         td.addProperty('value', String);
37         
38         return td;
39     }
40     Jeffz.Net.ServiceMethodRequest.registerBaseMethod(this, 'getDescriptor');
41 }
42 Jeffz.Net.Parameter.registerSealedClass('Jeffz.Net.Parameter', Sys.Component);
43 Sys.TypeDescriptor.addType('jeffz', 'param', Jeffz.Net.Parameter);

于是,相关的Declarative Syntax也会有所改变。但是请注意, 这只是我的一个尝试,这并不是一个好的做法,没有任何实用价值

我们来看一下它的使用,基本上代码没有变,只是改变了部分Atlas Xml Scripts:
29174333_Pu8c.gif 29174333_k7JL.gif Atlas Xml Scripts部分代码
 1 <script type="text/xml-script">
 2     <page xmlns:jeffz="http://www.jeffzlive.net">
 3         <components>
 4 
 5             ……
 6             
 7             <jeffz:serviceMethod id="employeeService" url="EmployeeService.asmx" methodName="AddEmployee" priority="High">
 8                 <parameters>
 9                     <jeffz:param name="name">
10                         <bindings>
11                             <binding dataContext="txtName" dataPath="text" property="value" />
12                         </bindings>
13                     </jeffz:param>
14                     <jeffz:param name="age">
15                         <bindings>
16                             <binding dataContext="txtAge" dataPath="text" property="value" />
17                         </bindings>
18                     </jeffz:param>
19                 </parameters>
20             </jeffz:serviceMethod>
21             
22             <listView id="listView" itemTemplateParentElementId="itemTemplateParent">
23                 <bindings>
24                     <binding dataContext="employeeService" dataPath="result" property="data" />
25                 </bindings>
26                 ……
27             </listView>
28         </components>
29     </page>
30 </script>

可能最值得注意的就是在<listView />内使用了Binding,现在一行Javascript代码都不用写就能运行了,这就是我们所需要的!对于现在的parameters的做法,再强调一下,对于目前的问题, 它没有任何实用价值

由于使用效果和上面一例完全相同,因此就不做演示了。:)


点击这里下载两个范例源文件。
点击这里查看“使用Declarative Syntax访问Web Services方法”范例效果。
点击这里查看“开发自己的Componet,完全使用Declarative Syntax访问Web Services方法”范例效果。


转载于:https://my.oschina.net/abcijkxyz/blog/721041

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值