FMS 调用 WEBSERVICE(2)

Prerequisite knowledge

You should be familiar with Macromedia Flash from Adobe and understand the basics of ActionScript in both Flash and Flash Media Server 2. It would also help to have a basic understanding of XML.

User level

Intermediate

Required products

Sample files

The design goal of any web service is to allow both developers and companies to share data easily. What makes this possible is the ability to output information in standard XML format so that programs written in different languages can communicate with the same service. You can also use your favorite language and platform to interface with Macromedia Flash Media Server 2 from Adobe.

Web services on the server side allow you to interact with your production servers without ever exposing the connection. By being free to develop in the language you want and still communicate with Flash Media Server, all you need to do is create a web service.

Flash Media Server 2 has some limitations on how web services handle complex data (objects) or an array returned by a web service. After some serious head scratching, I have attempted to draw out some of these limitations and describe to what extent you can still manipulate complex data types.

By way of example—a web service providing a weather forecast using the Web Service Description Language (WSDL)—this article attempts to explain not only how web services work in Flash Media Server 2 but detail some of the challenges you face when dealing with complex data types.

Exploring the simple forecast interface

In this section, I will go over the interface and the parameters required for the remote method.

There are many free web services on the Internet (XMethodsWebserviceX.net, and so on). I will use one of the WebserviceX.net services for this article (see Figure 1).

Weather Forecast application relying on Flash Media Server web services to display the information
Figure 1. Weather Forecast application relying on Flash Media Server web services to display the information

The end user simply submits a five-digit ZIP code to the service. There's no real error handling built in. When results are displayed, the Next and Prev buttons step the user through the upcoming days in the forecast.

Because there is a limit to the amount of data that Flash Media Server can receive from a web service, you may get mixed results with this weather forcast application. This web service provides a seven-day forecast but you will notice that, in most cases, you get a five-day forecast due to this limitation. The other result you may receive is an "Unable to connect to endpoint" message.

Note: If you do receive an "Unable to connect to endpoint" message, enter a new ZIP code and try again. There is no need to recompile the client application or unload the server ASC file. Alternate between ZIP codes 33021, 77077, 33004, 33309, 77079, 94105, 10286, and 97070. Of course, you can use your own ZIP code—just remember that this is a free service and is not always available.

Point your browser to the following URL, which shows the XML structure definition for the Weather Forecast WSDL (see Listing 1). You should be able to see other methods that you could use, like GetWeatherByPlaceName, which requires a parameter PlaceName of type string:

http://www.webservicex.net/WeatherForecast.asmx?WSDL

For this exercise, I will focus on GetWeatherByZipCode, which requires a parameter of type string.

Listing 1. Weather Forecast WSDL

<wsdl:definitions targetNamespace="http://www.webservicex.net"> <wsdl:types> <s:schema elementFormDefault="qualified" targetNamespace="http://www.webservicex.net"> <s:element name="GetWeatherByZipCode"> <s:complexType> <s:sequence> <s:element minOccurs="0" maxOccurs="1" name="ZipCode" type="s:string"/> </s:sequence> </s:complexType> </s:element> <s:element name="GetWeatherByZipCodeResponse"> <s:complexType> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="GetWeatherByZipCodeResult" type="tns:WeatherForecasts"/> </s:sequence> </s:complexType> </s:element> <s:complexType name="WeatherForecasts"> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="Latitude" type="s:float"/> <s:element minOccurs="1" maxOccurs="1" name="Longitude" type="s:float"/> <s:element minOccurs="1" maxOccurs="1" name="AllocationFactor" type="s:float"/> <s:element minOccurs="0" maxOccurs="1" name="FipsCode" type="s:string"/> <s:element minOccurs="0" maxOccurs="1" name="PlaceName" type="s:string"/> <s:element minOccurs="0" maxOccurs="1" name="StateCode" type="s:string"/> <s:element minOccurs="0" maxOccurs="1" name="Status" type="s:string"/> <s:element minOccurs="0" maxOccurs="1" name="Details" type="tns:ArrayOfWeatherData"/> </s:sequence> </s:complexType> <s:complexType name="ArrayOfWeatherData"> <s:sequence> <s:element minOccurs="0" maxOccurs="unbounded" name="WeatherData" type="tns:WeatherData"/> </s:sequence> </s:complexType> <s:complexType name="WeatherData"> <s:sequence> <s:element minOccurs="0" maxOccurs="1" name="Day" type="s:string"/> <s:element minOccurs="0" maxOccurs="1" name="WeatherImage" type="s:string"/> <s:element minOccurs="0" maxOccurs="1" name="MaxTemperatureF" type="s:string"/> <s:element minOccurs="0" maxOccurs="1" name="MinTemperatureF" type="s:string"/> <s:element minOccurs="0" maxOccurs="1" name="MaxTemperatureC" type="s:string"/> <s:element minOccurs="0" maxOccurs="1" name="MinTemperatureC" type="s:string"/> </s:sequence> </s:complexType> <s:element name="GetWeatherByPlaceName"> <s:complexType> <s:sequence> <s:element minOccurs="0" maxOccurs="1" name="PlaceName" type="s:string"/> </s:sequence> </s:complexType> </s:element> <s:element name="GetWeatherByPlaceNameResponse"> <s:complexType> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="GetWeatherByPlaceNameResult" type="tns:WeatherForecasts"/> </s:sequence> </s:complexType> </s:element> <s:element name="WeatherForecasts" type="tns:WeatherForecasts"/> </s:schema> </wsdl:types> <wsdl:message name="GetWeatherByZipCodeSoapIn"> <wsdl:part name="parameters" element="tns:GetWeatherByZipCode"/> </wsdl:message> <wsdl:message name="GetWeatherByZipCodeSoapOut"> <wsdl:part name="parameters" element="tns:GetWeatherByZipCodeResponse"/> </wsdl:message> <wsdl:message name="GetWeatherByPlaceNameSoapIn"> <wsdl:part name="parameters" element="tns:GetWeatherByPlaceName"/> </wsdl:message> <wsdl:message name="GetWeatherByPlaceNameSoapOut"> <wsdl:part name="parameters" element="tns:GetWeatherByPlaceNameResponse"/> </wsdl:message> <wsdl:message name="GetWeatherByZipCodeHttpGetIn"> <wsdl:part name="ZipCode" type="s:string"/> </wsdl:message> <wsdl:message name="GetWeatherByZipCodeHttpGetOut"> <wsdl:part name="Body" element="tns:WeatherForecasts"/> </wsdl:message> <wsdl:message name="GetWeatherByPlaceNameHttpGetIn"> <wsdl:part name="PlaceName" type="s:string"/> </wsdl:message> <wsdl:message name="GetWeatherByPlaceNameHttpGetOut"> <wsdl:part name="Body" element="tns:WeatherForecasts"/> </wsdl:message> <wsdl:message name="GetWeatherByZipCodeHttpPostIn"> <wsdl:part name="ZipCode" type="s:string"/> </wsdl:message> <wsdl:message name="GetWeatherByZipCodeHttpPostOut"> <wsdl:part name="Body" element="tns:WeatherForecasts"/> </wsdl:message> <wsdl:message name="GetWeatherByPlaceNameHttpPostIn"> <wsdl:part name="PlaceName" type="s:string"/> </wsdl:message> <wsdl:message name="GetWeatherByPlaceNameHttpPostOut"> <wsdl:part name="Body" element="tns:WeatherForecasts"/> </wsdl:message> <wsdl:portType name="WeatherForecastSoap"> <wsdl:operation name="GetWeatherByZipCode"> <documentation> Get one week weather forecast for a valid Zip Code(USA) </documentation> <wsdl:input message="tns:GetWeatherByZipCodeSoapIn"/> <wsdl:output message="tns:GetWeatherByZipCodeSoapOut"/> </wsdl:operation> <wsdl:operation name="GetWeatherByPlaceName"> <documentation> Get one week weather forecast for a place name(USA) </documentation> <wsdl:input message="tns:GetWeatherByPlaceNameSoapIn"/> <wsdl:output message="tns:GetWeatherByPlaceNameSoapOut"/> </wsdl:operation> </wsdl:portType> <wsdl:portType name="WeatherForecastHttpGet"> <wsdl:operation name="GetWeatherByZipCode"> <documentation> Get one week weather forecast for a valid Zip Code(USA) </documentation> <wsdl:input message="tns:GetWeatherByZipCodeHttpGetIn"/> <wsdl:output message="tns:GetWeatherByZipCodeHttpGetOut"/> </wsdl:operation> <wsdl:operation name="GetWeatherByPlaceName"> <documentation> Get one week weather forecast for a place name(USA) </documentation> <wsdl:input message="tns:GetWeatherByPlaceNameHttpGetIn"/> <wsdl:output message="tns:GetWeatherByPlaceNameHttpGetOut"/> </wsdl:operation> </wsdl:portType> <wsdl:portType name="WeatherForecastHttpPost"> <wsdl:operation name="GetWeatherByZipCode"> <documentation> Get one week weather forecast for a valid Zip Code(USA) </documentation> <wsdl:input message="tns:GetWeatherByZipCodeHttpPostIn"/> <wsdl:output message="tns:GetWeatherByZipCodeHttpPostOut"/> </wsdl:operation> <wsdl:operation name="GetWeatherByPlaceName"> <documentation> Get one week weather forecast for a place name(USA) </documentation> <wsdl:input message="tns:GetWeatherByPlaceNameHttpPostIn"/> <wsdl:output message="tns:GetWeatherByPlaceNameHttpPostOut"/> </wsdl:operation> </wsdl:portType> <wsdl:binding name="WeatherForecastSoap" type="tns:WeatherForecastSoap"> <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/> <wsdl:operation name="GetWeatherByZipCode"> <soap:operation soapAction="http://www.webservicex.net/GetWeatherByZipCode" style="document"/> <wsdl:input> <soap:body use="literal"/> </wsdl:input> <wsdl:output> <soap:body use="literal"/> </wsdl:output> </wsdl:operation> <wsdl:operation name="GetWeatherByPlaceName"> <soap:operation soapAction="http://www.webservicex.net/GetWeatherByPlaceName" style="document"/> <wsdl:input> <soap:body use="literal"/> </wsdl:input> <wsdl:output> <soap:body use="literal"/> </wsdl:output> </wsdl:operation> </wsdl:binding> <wsdl:binding name="WeatherForecastHttpGet" type="tns:WeatherForecastHttpGet"> <http:binding verb="GET"/> <wsdl:operation name="GetWeatherByZipCode"> <http:operation location="/GetWeatherByZipCode"/> <wsdl:input> <http:urlEncoded/> </wsdl:input> <wsdl:output> <mime:mimeXml part="Body"/> </wsdl:output> </wsdl:operation> <wsdl:operation name="GetWeatherByPlaceName"> <http:operation location="/GetWeatherByPlaceName"/> <wsdl:input> <http:urlEncoded/> </wsdl:input> <wsdl:output> <mime:mimeXml part="Body"/> </wsdl:output> </wsdl:operation> </wsdl:binding> <wsdl:binding name="WeatherForecastHttpPost" type="tns:WeatherForecastHttpPost"> <http:binding verb="POST"/> <wsdl:operation name="GetWeatherByZipCode"> <http:operation location="/GetWeatherByZipCode"/> <wsdl:input> <mime:content type="application/x-www-form-urlencoded"/> </wsdl:input> <wsdl:output> <mime:mimeXml part="Body"/> </wsdl:output> </wsdl:operation> <wsdl:operation name="GetWeatherByPlaceName"> <http:operation location="/GetWeatherByPlaceName"/> <wsdl:input> <mime:content type="application/x-www-form-urlencoded"/> </wsdl:input> <wsdl:output> <mime:mimeXml part="Body"/> </wsdl:output> </wsdl:operation> </wsdl:binding> <wsdl:service name="WeatherForecast"> <documentation> Get one week weather forecast for valid zip code or Place name in USA </documentation> <wsdl:port name="WeatherForecastSoap" binding="tns:WeatherForecastSoap"> <soap:address location="http://www.webservicex.net/WeatherForecast.asmx"/> </wsdl:port> <wsdl:port name="WeatherForecastHttpGet" binding="tns:WeatherForecastHttpGet"> <http:address location="http://www.webservicex.net/WeatherForecast.asmx"/> </wsdl:port> <wsdl:port name="WeatherForecastHttpPost" binding="tns:WeatherForecastHttpPost"> <http:address location="http://www.webservicex.net/WeatherForecast.asmx"/> </wsdl:port> </wsdl:service></wsdl:definitions>

Examining the server-side code

On the server side, make sure you start by loading the WebServices class with the following:

load("webservices/WebServices.asc");

In this particular example, you are using remote method invocation (RMI) from the client to the server methodgetWeather:

Client.prototype.getWeather = function (zip){

Then you need a var of type string and array as well as the path to your service:

var aDetail=[];var Detailing="";var WSDLuri = "http://www.webservicex.net/WeatherForecast.asmx?WSDL";

Instantiate the WebServices class as follows, passing in the path to your service:

WeatherService = new WebService(WSDLuri);

Keep in mind that the WebServices class of Flash Media Server 2 provides you with two event handlers: onLoad andonFault for both the WSDL and the result.

So you could use the following:

WeatherService.onLoad = function(Wsdl){ trace(Wsdl); } // onFault is triggered if the above load fails WeatherService.onFault = function(fault){ trace( fault.faultstring ); clientNow.call("failed", null, fault.faultstring); }

Once your WebService object is set, you need to fire an event on it using a function provided by the web services provider. Pointing your browser to the WSDL URL, you can view the XML definition as well as other descriptions. This allows you to view all available methods and their corresponding results.

As shown in Listing 1, pointing the browser to the URL reveals which methods are available to call within the XML code definition presented by the browser and how they will return the results.

Currently, you are concerned with the GetWeatherByZipCode method:

forcast =WeatherService.GetWeatherByZipCode(zip);

Once your method is called, you need to parse the result when onResult is triggered:

forcast.onResult = function(returning) { //convert result into an array aDetail for (var i in returning) { if (i == "Details") { for (var j in returning[i]) { switch (j) { case "xmlNodes" : //get rid of commas for (var p in returning[i][j]) { if (returning[i][j][p] != undefined) { aDetail.push(returning[i][j][p].toString()); } } case "length" : var total = returning[i][j]; } } } } var tot= aDetail.length; //push each array slot into the string Detailing to form xml string for (var i = 1; i<tot; ++i) { Detailing += aDetail[i]; } aDetail.splice(0); //now send xml string to client clientNow.call("forcast", null, Detailing); }

This process is rather elementary. All you're doing is working around the current WebServices class to handle an array. You use a "for in" loop to grab each element in the xmlNodes—part of the Details result—and pass it into the aDetailarray you created earlier:

for (var p in returning[i][j]) { if (returning[i][j][p] != undefined) { aDetail.push(returning[i][j][p].toString()); }}

Once you have passed in all nodes, you can manipulate the array directly. Keep in mind that every web service returns results in its own way, so it is important to find a way to unfold your result. I have found that the most reliable way is to use the "for in" loop. Applying a "for in" loop showed some promise to the wealth of data returned. I then found other objects and set up "for in" loops in them, as well, until I had all the data I needed.

Note: If you handle the processing on the client side, the "for in" loops will read backwards. Therefore, it is important to reverse the array (using array.reverse()), once you have pushed the nodes into the array, to correct the order.

At this point you could use just the array to do what you want but my main objective is to get the data into its proposed XML format:

for (var i = 1; i<tot; ++i) { Detailing += aDetail[i];}

Push each node into the Detailing string and send to the client:

clientNow.call("forcast", null, Detailing);

Examining the client-side code

As you may be able to determine in Figure 1, I used three buttons ("Prev," "Next," and "Get Forecast"), a text input field (z_text), and a text area (T_text):

  • prev button has the label "prev"
  • dnxt has the label "next"
  • B has the label "Get Forecast"
  • z_text input field is for the ZIP code
  • T_text text area displays the output information

In the Main.as class I have the following:

m_nc.forcast = function(returning):Void { this.owner.ReturnForcast(returning); };

which passes results to its own ReturnForcast method:

private function ReturnForcast(Detailing):Void { B.enabled = true; var my2_xml:XML = new XML(Detailing); var parentNode = my2_xml.childNodes; var xTotal:Number = parentNode.length; for (var i = 0; i<xTotal; ++i) { var subs:Number = parentNode[i].childNodes.length; var day = parentNode[i].childNodes[0].firstChild.nodeValue; var wImg = parentNode[i].childNodes[1].firstChild.nodeValue; var hF = parentNode[i].childNodes[2].firstChild.nodeValue; var lF = parentNode[i].childNodes[3].firstChild.nodeValue; var hC = parentNode[i].childNodes[4].firstChild.nodeValue; var lC = parentNode[i].childNodes[5].firstChild.nodeValue; aWeather.push([day, wImg, hF, lF, hC, lC]); }T_text.html = true;showWeather(); }

Once ReturnFunction uses the Flash Media Server string to create your XML, you can manipulate it into an array to be processed by showWeather(). As I stated earlier, you could have also just sent the initial array from the server:

private function showWeather() { if (aWeather[current][0] != undefined) { T_text.text = ""; T_text.text += aWeather[current][0]+"<br>"; T_text.text += "<img src='"+aWeather[current][1]+"'><br>"; T_text.text += "high F="+aWeather[current][2]+"<br>"; T_text.text += "low F="+aWeather[current][3]+"<br>"; T_text.text += "high C="+aWeather[current][4]+"<br>"; T_text.text += "low C="+aWeather[current][5]+"<br>"; } prev.enabled =(current>0? true:false); dnxt.enabled =(current<aWeather.length?true:false); }

To make the actual remote method call from the client, use the B("Get Forecast") button's click event:

private function sendBtn() { var scope = this._parent; scope.current = 0; if (scope.z_text.text.length == 5) { scope.prev.enabled = false; scope.dnxt.enabled = false; scope.B.enabled = false; scope.m_nc.call("getWeather", null, scope.z_text.text); scope.T_text.text = "please wait\n LOADING...."; } else { scope.T_text.text = "enter your 5digit zip code above"; }}

Reviewing the final code

Your final client-side code should look like this:

import mx.controls.*;//manipulation on Server sideclass Main extends MovieClip { private var current:Number = 0; private var Kconn:String = "rtmp:/webSD"; private var aWeather:Array = new Array(); private var T_text:TextArea; private var z_text:TextInput; private var B:Button; private var prev:Button; private var dnxt:Button; private var m_nc:NetConnection; function Main(Void) { this.attachMovie("TextInput", "z_text", 2,{_y:15}); z_text.setSize(74,22); this.attachMovie("TextArea", "T_text",4,{_y:z_text._y+z_text.height+3}); T_text.setSize(178,141); T_text.html=false; T_text.wordWrap=true; trace("constructor inititalized!"); } //Called by the main timeline function connectMe(Void):Void { m_nc = new NetConnection(); m_nc.owner = this; m_nc.onStatus = function(info:Object):Void { this.owner.onConnectionStatus(info); }; m_nc.forcast = function(returning):Void { trace("try returning from server"+returning); this.owner.ReturnForcast(returning); }; m_nc.failed = function(failedString:String):Void { this.owner.Reset(failedString); }; m_nc.connect(Kconn); } private function onConnectionStatus(info:Object):Void { trace("nc.onStatus> info.code: "+info.code); if (info.code == "NetConnection.Call.Failed") { Reset(info.code); } if (info.description) { trace("nc.onStatus> info.description: "+info.description); } } //recieve forcast from the Server's webservice private function ReturnForcast(Detailing):Void { B.enabled = true; var my2_xml:XML = new XML(Detailing); var parentNode = my2_xml.childNodes; var xTotal:Number = parentNode.length; for (var i = 0; i<xTotal; ++i) { var subs:Number = parentNode[i].childNodes.length; var day = parentNode[i].childNodes[0].firstChild.nodeValue; var wImg = parentNode[i].childNodes[1].firstChild.nodeValue; var hF = parentNode[i].childNodes[2].firstChild.nodeValue; var lF = parentNode[i].childNodes[3].firstChild.nodeValue; var hC = parentNode[i].childNodes[4].firstChild.nodeValue; var lC = parentNode[i].childNodes[5].firstChild.nodeValue; aWeather.push([day, wImg, hF, lF, hC, lC]); } //aWeather.reverse(); //trace("003 "+aWeather); T_text.html = true; showWeather(); } //produce visual representation of data stored in array private function showWeather() { //trace("004--> showWeather()"+current); if (aWeather[current][0] != undefined) { //::TODO proper error handling //(current == aWeather.length ? --current : ++current); T_text.text = ""; T_text.text += aWeather[current][0]+"<br>"; T_text.text += "<img src='"+aWeather[current][1]+"'><br>"; T_text.text += "high F="+aWeather[current][2]+"<br>"; T_text.text += "low F="+aWeather[current][3]+"<br>"; T_text.text += "high C="+aWeather[current][4]+"<br>"; T_text.text += "low C="+aWeather[current][5]+"<br>"; } prev.enabled =(current>0? true:false); dnxt.enabled =(current<aWeather.length?true:false); } private function Reset(failed:String) { trace("reset called"); T_text.text = failed; B.enabled = true; } //load public function onLoad(){ B.addEventListener("click",this.sendBtn); prev.addEventListener("click",this.prevBtn); dnxt.addEventListener("click",this.nextBtn); B.label = "Get Forecast"; prev.label = "prev"; dnxt.label = "next"; } //next button call private function nextBtn() { var scope = this._parent; ++scope.current; scope.showWeather(); } //prev button call private function prevBtn() { var scope = this._parent; --scope.current; scope.showWeather(); } //send(B) zip button call private function sendBtn() { var scope = this._parent; scope.current = 0; if (scope.z_text.text.length == 5) { scope.prev.enabled = false; scope.dnxt.enabled = false; scope.B.enabled = false; scope.m_nc.call("getWeather", null, scope.z_text.text); scope.T_text.text = "please wait\n LOADING...."; } else { scope.T_text.text = "enter your 5digit zip code above"; } }}

Your final server-side code should look like this:

//most methods directly reflect the client side methods of webservicesload("webservices/WebServices.asc");application.onAppStart = function(){ }application.onConnect = function(N_client){ trace("user has connected"); application.acceptConnection(N_client); }Client.prototype.getWeather = function (zip){ var aDetail=[]; var Detailing=""; var clientNow=this; var WSDLuri = "http://www.webservicex.net/WeatherForecast.asmx?WSDL"; // Service Created WeatherService = new WebService(WSDLuri); // load WSDL WeatherService.onLoad = function(Wsdl){ trace("wsdl-->string"+Wsdl); } // onFault is triggered if the above load fails WeatherService.onFault = function(fault){ trace("wetherservice fault-->"+ fault.faultstring ); clientNow.call("failed", null, fault.faultstring); } //forcast is set to recieve result from the remote method "GetWeatherByZipCode" forcast =WeatherService.GetWeatherByZipCode(zip); // Grab the result forcast.onResult = function(returning) { //convert result into an array aDetail for (var i in returning) { if (i == "Details") { for (var j in returning[i]) { switch (j) { case "xmlNodes" : //get rid of commas for (var p in returning[i][j]) { if (returning[i][j][p] != undefined) { aDetail.push(returning[i][j][p].toString()); } } case "length" : var total = returning[i][j]; } } } } var tot= aDetail.length; //push each array slot into the string "Detailing" to form xml string for (var i = 1; i<tot; ++i) { Detailing += aDetail[i]; } aDetail.splice(0); //now send xml string to client clientNow.call("forcast", null, Detailing); } // if Above fails provide error forcast.onFault = function(fault){ trace( "fault->"+fault.faultstring ); clientNow.call("failed", null, fault.faultstring); }}

Where to go from here

This simple example should provide you with a better idea of the web services limitations that you will encounter. Having taken the time to explore the possibilities, however, you can see that when you use Flash Media Server 2, you have the freedom to interface with not only any language but with any result set as well.

For more information, check out the following resources for various technologies:

You may also wish to read up on XML-RPC, a "spec and set of implementations that allow software running on disparate operating systems, running in different environments to make procedure calls over the Internet."

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值