grails创建程序_用Grails和Flex编写JEE应用程序

grails创建程序

Java平台已经发展成为一个坚实而成熟的企业应用程序平台。 成熟的应用程序平台的标志之一是,有许多衍生技术和与其他技术集成的选项。 本文将详细介绍如何使用Grails(传统的JEE应用程序开发的衍生产品)和Flex(可用于Java的另一种技术)编写JEE应用程序。 这两个框架都可以高效地工作。 将这两个框架结合在一起,有望在保持高生产率的同时为J2EE应用程序构建丰富的Internet前端。

Grails最初是一个Web应用程序框架,它使用Groovy和诸如Spring和Hibernate之类的知名框架在JVM中运行。 它在很大程度上依赖于“ Convention over Configuration”原则,以允许非常快速的应用程序开发。 Groovy的附加功能及其众多动态特性使该应用程序框架在定义组件之间的通用行为方面极为强大。 Grails使用的插件架构使与其他框架的集成以及在应用程序之间重用功能非常容易。

Flex是一个RIA开发工具包,用于创建在FlashPlayer中运行的SWF应用程序。 它是Adobe(以前的MacroMedia)提供的Flash开发工具包的新版本。 除了拥有丰富的小部件集和将这些小部件粘合在一起的强大语言之外,它还具有一些先进的通信解决方案,使开发分布式应用程序变得更加容易。 它使用两种类型的语法:MXML和ActionScript。 MXML是一种基于XML的语法,用于从通用组件定义用户界面的构建。 ActionScript用于定义这些组件上的动态行为。

整合Grails和Flex-问题

当结合基于Grails和Flex等不同基础构建的两个框架时,会出现一些主要与通信有关的问题:

  1. 一个框架中的组件如何找到另一框架中与之通信的正确组件?
    Grails本质上是一个在服务器上的JVM中运行的Web应用程序框架。 Flex是一个具有客户端和(瘦)服务器组件的RIA平台。 服务器组件被部署为Web应用程序。 因此,两个框架之间的集成是在Web应用程序容器内进行的。
    用户通过Flex UI启动的通信必须到达Grails组件才能调用某些业务逻辑。 Flex UI组件如何到达正确的Grails组件?
  2. 框架之间的数据如何转换?
    Flex使用ActionScript进行数据表示。 Grails使用Java和Groovy对象。 Flex UI发送到服务器的ActionScript对象应转换为有意义的依赖于应用程序的数据结构。 如何才能做到这一点?
  3. 某个用户所做的更改如何通过应用程序传达给其他用户?
    尽管对于多用户应用程序来说这是一个普遍的问题,但是使用两个不同的框架使寻找解决方案更加困难。 更改是在Grails应用程序中进行的(由Flex UI中的用户操作发起),应通过其Flex UI传达给许多用户。 如何做到这一点?

接下来的三章将更详细地讨论上述问题,并尝试使用Grails和Flex提出答案。

整合-寻找讯息的目标

一个框架中的组件如何找到另一框架中与之通信的正确组件?

当应用于Grails和Flex时,此问题是有关Flex组件如何找到合适的Grails组件来代表用户请求数据或执行操作的问题。 为了更好地了解如何解决此问题,我们首先将仔细研究Flex的通信子系统。

Flex中的客户端-服务器通信

Flex中的通信子系统可以分为客户端部分和服务器部分。 客户端部分包含允许应用程序发送或接收消息的组件,例如RemoteObject或Consumer组件。 这些组件与服务器部分中的特定“服务”对象相关联,例如RemotingService和MessagingService。 客户端组件及其关联的服务组件的组合支持典型的通信模式。 例如,使用Consumer,Producers和MessagingService,允许应用程序使用Publish-Subscribe机制进行通信。

客户端和服务器之间的通信通过通道进行。 有几种渠道的实现。 最重要的是AMFChannel和RTMPChannel。 AMFChannel基于HTTP,因此基于请求-响应体系结构。 此通道可以与MessagingService一起使用以支持Publish-Subscribe。 在此组合请求中,将定期通过通道进行请求,以获取已发布的新消息。 RTMPChannel在此设置中效率更高,它支持基于TCP / IP的客户端和服务器之间的开放连接。 以这种方式,它允许在两个方向上立即发送和接收消息。 不幸的是,BlazeDS是Adobe Flex的免费开源实现,它不包含RTMPChannel的实现。

Flex的通信基础结构中最重要的部分是目的地。 这些是通信通道的服务器端端点。 服务提供目标,并且客户端组件通过目标附加到服务。 邮件可以通过附加的客户端组件发送到目标并从目标接收。 目的地可以由工厂创建。

Grails中的远程曝光:服务

Flex的复杂通信基础架构应如何与Grails结合? Grails可以识别几种类型的对象:域对象,控制器,视图和服务。 服务是Grails中的一个对象,该对象通过外部通信渠道(例如HTTP)公开一些功能或服务。 就Flex而言,服务与目标相对应。

这正是Grails的flex-plugin提供的解决方案。 Grails中所有标记为可暴露于Flex的服务都将在Flex框架中注册为Destinations。 Grails使用在Flex中配置的特殊RemotingService,所有标记的服务将由特定Factory添加到其中。 该工厂将在Grails使用的Spring上下文中找到相应的服务。 所有这些配置都可以在用于Grails的flex-plugin复制到正确位置的services-config.xml文件中找到。

class UserService {
  static expose = [ 'flex-remoting' ]
  def List all() {
    User.createCriteria().listDistinct {}
  }
  def Object get(id) {
    User.get(id);
  }
  def List update(User entity) throws BindException {
    entity.merge();
    if (entity.errors.hasErrors()) {
      throw new BindException(entity.errors);
    }
    all();
  }
  def List remove(User entity) {
    entity.delete();
    all();
  }
}

此配置将用户服务公开给Flex客户端中的RemoteObjects。 以下MXML片段显示了如何使用它。 RemoteObject的目的地是“ userService”,它是Grails中目标对象的服务名称。 服务对象上的所有方法都已成为远程操作。 可以从ActionScript中将这些操作用作常规方法,并且将像常规ActionScript事件一样处理结果或错误。

...<mx:RemoteObject id=" service " destination=" userService ">
    <mx:operation name=" all " result=" setList(event.message.body) "/>
    <mx:operation name=" get " result=" setSelected(event.message.body) "/>
    <mx:operation name=" update "/>
    <mx:operation name=" remove "/>
  </mx:RemoteObject >
...

结论

用于Grails的flex-plugin解决集成问题的解决方案非常优雅。 它非常易于使用,几乎是自动的。 本着“约定优于配置”的精神,当将目的地动态添加到Flex配置时,将使用命名约定。

资料转换

如何在框架之间转换数据(在这种情况下为Java和ActionScript对象)?

要分析此问题,重要的是要认识到这两个框架之间的联系。 Flex由Java组件(在Web服务器上)和ActionScript组件(在客户端上)组成。 因此,Grails和Flex之间的边界位于Web服务器上,并且实际上在两侧都是Java应用程序。

Flex的Java组件仅专注于与Flex客户端的通信。 AMF协议用于此数据通信,并且基于ActionScript对象。 服务器组件中的Java代码将数据转换为ActionScript对象,并通过通道对其进行序列化。 Flex开箱即用地支持原始和标准的复杂Java类型(例如Date或Collection)。 由于ActionScript是一种动态语言,因此也支持随机对象结构。 Java对象的字段将转换为ActionScript对象上的动态属性。 将这些非类型的ActionScript对象转换回Groovy域对象则不太直接。 默认情况下,会创建一个Map,在其中将属性存储为键值参数。

通过创建与Groovy域对象具有相同属性的ActionScript类,并通过注释链接它们,Flex可以使转换更加平稳。 下面显示了此类Groovy-ActionScript对的示例。

Groovy 动作脚本
class User implements Serializable {
    String username
    String password
    String displayName
}
[RemoteClass (alias="User")]
public class User {
  public var id:*
  public var version:*
  public var username:String;
  public var password:String = "";
  public var displayName:String;


  public function toString():String {
    return displayName;
  } 
}

“ RemoteClass”注解将ActionScript类链接到别名属性指示的Java(或Groovy)类。 此属性应包含完全限定的类名。 在Grails中,域类通常添加到默认包中。 Grails类中的所有字段都将复制到ActionScript类。 名称应完全匹配。 由Grails动态添加到每个域类的“ id”和“ version”字段也应添加,以在与客户端通信期间保留此信息。

结论

Flex提供的Java(或Groovy)数据转换解决方案导致大量代码重复。 每个域类应定义两次,一次在Groovy(或Java)中定义,一次在ActionScript中定义。 这提供了将客户端特定的代码(例如,与显示对象有关的代码)仅添加到ActionScript代码的可能性。 它还使编辑者可以提供两种语言的代码完成功能。 使用注释进行配置非常方便。

多用户

某个用户所做的更改如何通过应用程序传达给其他用户?

在同时支持多个用户的应用程序中,挑战之一是将一个用户对共享数据所做的更改传达给其他用户。 对于其他用户,这可以看作是服务器启动的通信。

从单个中心点(服务器)与许多接收者(客户端)进行通信时,使用发布-订阅技术通常会很有帮助。 通过这种技术,客户端可以向服务器注册(订阅)。 当有趣的消息发布到服务器时,将通知他们。

由于可以从Grails使用Java,因此可以使用JMS。 JMS是用于在应用程序之间进行消息传递的Java标准,它支持发布-订阅。 Flex具有自己的消息传递组件,该组件支持发布-订阅,但是也可以通过适配器与JMS集成。

在Grails中配置JMS

对于大多数标准,有一个用于Grails的jms插件 ,它添加了许多有用的方法来将消息发送到所有控制器和服务类的JMS目标。 现在,可以通过UserService使用这些方法(如上一章所述),以通过JMS进行更改时将更新发送给所有客户端。

class UserService {
  ...
  def List update(User entity) throws  BindException {
    entity.merge(flush: true );
    if  (entity.errors.hasErrors()) {
      throw new BindException(entity.errors)
    }
    sendUpdate();
    all();
  }
  def List remove(User entity) {
    entity.delete(flush: true );
    sendUpdate();
    all();
  }
  private def void sendUpdate() {
    try  {
      sendPubSubJMSMessage( "tpc" ,all(),[type: "User" ]);
    } catch (Exception e) {
      log.error( "Sending updates failed." , e);
    }
  }
}

该服务可以确定何时应发送什么消息。 每当客户端更新或删除数据时,都会发送一条消息,其中包含数据项的完整列表。 数据将发送到指定的主题,在本例中为“ tpc”。 为此主题注册的任何接收者都将接收新数据。 列表中的对象类型(在这种情况下为“用户”)作为元数据添加到消息中,以使接收者在向服务器注册时可以表明他们对特定数据类型的兴趣。

为了能够在Grails应用程序中使用JMS,需要使JMS提供程序实现可用。 Apache提供了一个免费的开放源代码实现,可以从Grails应用程序轻松配置它。 通过将ApacheMQ库添加到Grails应用程序的lib目录中,并将下面的片段添加到conf / spring目录中的resources.xml文件中,即可使用连接工厂。

...<bean id = "connectionFactory"
class ="org.apache.activemq.pool.PooledConnectionFactory"
destroy-method= "stop">
<property name ="connectionFactory">
<bean class= "org.apache.activemq.ActiveMQConnectionFactory">
<property name= "brokerURL" value= "vm://localhost"
/> </bean> </property> </bean> ...

在Flex中接收JMS消息

flex的配置当前仅包含RemotingService,该服务支持用于与UserService进行通信的请求-响应样式通信。 该服务是由Grails的flex-plugin添加的。 另外,我们现在需要一个MessagingService来支持发布-订阅样式的通信。

...<service id ="message-service" class ="flex.messaging.services.MessageService" messageTypes ="flex.messaging.messages.AsyncMessage">

<adapters>
<adapter-definition id ="jms" class ="flex.messaging.services.messaging.adapters.JMSAdapter" default ="true"/>
</adapters> <destination id ="tpc">
<properties>
<jms>
<message-type> javax.jms.ObjectMessage </message-type> <connection-factory> ConnectionFactory </connection-factory> <destination-jndi-name> tpc </destination-jndi-name> <delivery-mode> NON_PERSISTENT </delivery-mode> <message-priority> DEFAULT_PRIORITY </message-priority> <acknowledge-mode> AUTO_ACKNOWLEDGE </acknowledge-mode> <transacted-sessions> false </transacted-sessions> <initial-context-environment> <property> <name> Context.PROVIDER_URL </name> <value> vm://localhost </value> </property> <property> <name> Context.INITIAL_CONTEXT_FACTORY </name> <value> org.apache.activemq.jndi.ActiveMQInitialContextFactory </value> </property> <property> <name> topic.tpc </name> <value> tpc </value> </property>
</initial-context-environment>
</jms>
</properties>
</destination>

</service>
...

我们将以下片段添加到services-config.xml中,该片段包含带有JMSAdapter的新MessagingService。 该适配器将服务中的目标链接到JMS资源。 该服务还包含弹性代码中的使用者可以预订的目的地配置。 目标包含许多JMS特定的配置。 大多数是众所周知的JMS属性。 initial-context-environment中的“ topic.tpc”属性是一个自定义ActiveMQ属性,它将在上下文中注册一个JNDI名称为“ tpc”的主题。

...<mx:Consumer destination=" tpc " selector=" type = 'User' "
    message=" setList(event.message.body) "/>
...

Flex客户端代码非常简单。 消费者组件接收符合选择器的发送到指定目的地的消息。 在这种情况下,我们使用选择器来指定此使用者对“类型”元数据属性设置为“用户”的消息感兴趣。 每当接收到一条消息时,该消息的内容(应该是用户对象的列表)就会放在可以显示的内部列表中。 消息内容的处理与RemoteObject上“ all”操作的返回值完全相同。

结论

可以完全从标准组件构建使用Grails和Flex将更改传达给多个用户的解决方案。 涉及的组件数量使配置和实现变得相当复杂。 如果配置正确,则使用此解决方案非常简单。

结合解决方案

回顾最后三章中的解决方案,似乎有可能将它们组合为通用解决方案,以在Flex / Grails应用程序中在客户端和服务器之间传递域状态。 在本章中,我们将研究这种通用解决方案的外观。

泛化服务器端代码

在服务器端运行的用于解决问题1和3的代码已经合并到一个Groovy服务中。 现在,此服务专用于用户域类。 使用Groovy作为一种动态语言,可以很容易地将该服务推广到适用于所有域类。

import org.codehaus.groovy.grails.commons.ApplicationHolder

class CrudService {
  static expose = ['flex-remoting']

  def List all(String domainType) {
    clazz(domainType).createCriteria().listDistinct {}
  }

  def Object get(String domainType, id) {
    clazz(domainType).get(id)
  }

  def List update(String domainType, Object entity)
      throws BindException {
    entity.merge(deepValidate: false , flush: true )
    if (entity.errors.hasErrors()) {
      throw new BindException(entity.errors)
    }
    sendUpdate(domainType);
    all(domainType);
  }

  def List remove(String domainType, Object entity) {
    entity.delete(flush: true );
    sendUpdate(domainType);
    all(domainType);

  }
  private def Class clazz(className) {
    return ApplicationHolder.application.getClassForName(className);
  }

  private def void sendUpdate(String domainType) {
    try {
      sendPubSubJMSMessage(" tpc ", all(domainType), [type:domainType]);
    } catch (Exception e) {
      log.error( "Sending updates failed." , e);
    }
  }
}

实现此目的的主要技巧是让客户端决定要返回的域类型。 为此,向所有服务引入了一个参数,该参数将标识服务器的域类型。 域类型的类名显然是用于此参数的选择。 结果服务将在所有域对象上提供C(reate)R(etrieve)U(pdate)D(elete)操作,因此可以称为CrudService。

进行更改时,CrudService将向JMS主题发送更新。 此更新包含应用程序当前已知的域对象的完整列表。 为了使客户端可以轻松地决定哪些更新对他们而言很有趣,将域类型的类的名称作为元数据添加到消息中。

客户端代码

解决方案1和3的客户端ActionScript代码也可以组合到一个类中。 然后,可以使用此类的实例来管理客户端上某个域类型的所有实例的集合。

public class DomainInstancesManager
{
  private var domainType : String;
  public function EntityManager(domainType : String, destination : String) {
    this.domainType = domainType;
    initializeRemoteObject();
    initializeConsumer(destination);
  }

  private var  _list : ArrayCollection = new ArrayCollection();
  public function get list () : ArrayCollection {
    return _list;
  }
  private function setList(list : *) : void {
    _list.removeAll();
    for each ( var o : * in list) {
      _list.addItem(o);
    }
  }

  internal static function defaultFault(error : FaultEvent) : void {
    Alert.show(" Error while communicating with server : " + error.fault.faultString);
  }
  ...
}

客户端的ActionScript实现基本上由两个组件组成:促进请求-响应对话的RemoteObject和促进生产者与订户对话的使用者。 在前面的章节中,这些对象是从MXML代码初始化的,但是也可以在ActionScript中创建它们。 上面的代码片段显示了两个组件都使用的通用结构:包含实例和错误处理的列表。 带有实例的列表将通过来自任何一个通信组件的消息进行更新。

...private var consumer : Consumer;
  private function initializeConsumer(destination : String) : void {
    this .consumer = new Consumer();
    this .consumer.destination = destination;
    this .consumer.selector = "type ='" + domainType + "'";
    this .consumer.addEventListener(MessageEvent.MESSAGE, setListFromMessage);
    this .consumer.subscribe();
  }

  private function setListFromMessage(e : MessageEvent) : void {
    setList(e.message.body);
  }
...

此代码段显示了如何从ActionScript构造消费者,该消费者将用于接收从服务器推送的消息。 消费者上的选择器属性设置为仅接收包含指定domainType作为元数据类型的消息。 每当收到这样的消息时,事件处理程序都会被调用,这将更新列表。

接下来的代码片段与将RemoteObject设置为请求-响应样式通信的端点有关。 所有必需的操作都将添加到RemoteObject的operation属性中,因此可以轻松地调用它们。

...private var service : RemoteObject;
private var getOperation : Operation = new Operation();
public function initializeRemoteObject() {
	this .service = new RemoteObject("crudService");

	var operations:Object = new  Object();
	operations[ "all" ] =  new  Operation();
	operations[ "all" ].addEventListener(ResultEvent.RESULT, setListFromInvocation);
	operations[ "get" ] = getOperation
	operations[ "remove" ] = new  Operation()
	operations[ "remove" ].addEventListener(ResultEvent.RESULT, setListFromInvocation);
	operations[ "update" ] = new  Operation()
	operations[ "update" ].addEventListener(ResultEvent.RESULT, setListFromInvocation);
	this  .service.operations = operations;
	this  .service.addEventListener(FaultEvent.FAULT, defaultFault);

	// Get the instances from the server.
	this .service.all(domainType);
	}

public function get(id : *, callback : Function) : void {
	var future: AsyncToken = getOperation.send(domainType, id);
	future.addResponder(new CallbackResponder(callback));
}

public function update(entity : Object) : void {
	service.update(domainType, entity);
}

public function remove(entity : Object) : void {
	service.remove(domainType, entity);
}

private function setListFromInvocation(e : ResultEvent) : void {
	setList(e.message.body);
}
...

大多数方法只是委托给服务的一种操作。 所有这些操作都是非阻塞且异步的。 每当服务返回时,返回值都由注册的事件处理程序(setListFromInvocation)处理,该事件处理程序将更新列表。 “ getOperation”有点例外,因为结果在多个地方使用。 为了获得结果,必须为每个调用注册一个CallbackResponder来处理结果。 响应者将使用接收到的消息内容调用功能。

import  mx.rpc.IResponder;
import  mx.rpc.events.ResultEvent;

public  class CallbackResponder implements  IResponder {
  private  var callback : Function;
  function CallbackResponder(callback : Function) {
    this  .callback = callback;
  }

  public  function result(data : Object) : void  {
    callback(ResultEvent(data).message.body);
  }

  public  function fault(info : Object) : void  {
    DomainInstancesManager.defaultFault(info);
  }
}

使用通用包

那么,您将如何使用该通用软件包? 让我们看一个在第二个解决方案中看到的管理User对象实例的示例。 下面的MXML代码定义了一个PopUpDialog,可用于编辑系统中“用户”的详细信息。 对话框看起来就像左边的图像。 实例变量“ manager”被初始化为User域类型的DomainInstanceManager。 该窗口包含绑定到该管理器的list属性的所有用户的列表。 它显示用户的displayName。

<mx:TitleWindow xmlns:mx=" http://www.adobe.com/2006/mxml " xmlns:users=" users.* " title=" User Manager ">
  <mx:Script>
    <![CDATA[
      import crud.DomainInstancesManager;
      import mx.managers.PopUpManager;
      [ Bindable ]
      private var  manager : DomainInstancesManager = new DomainInstancesManager(" User ", " tpc ");

      private function resetForm() : void {
        selectedUser = new User();
        secondPasswordInput.text = "";
      }

      private function  setSelected(o : Object) : void
      {
        selectedUser = User(o);
        secondPasswordInput.text = selectedUser.password;
      }
    ]]>
  </mx:Script>
  <users:User id=" selectedUser "
    displayName="{displayNameInput.text}"
    username="{usernameInput.text}"
    password="{passwordInput.text}"/>
  <mx:List height=" 100% " width=" 200 " dataProvider=" { manager.list } " labelField=" displayName "
    itemClick="manager.get(User(event.currentTarget.selectedItem).id, setSelected)"/>
  <mx:VBox height=" 100% " horizontalAlign=" right ">
    <mx:Form>
      <mx:FormItem label=" Display Name ">
        <mx:TextInput id=" displayNameInput " text="{selectedUser.displayName}"/>
      </mx:FormItem>
<mx:FormItem
label=" User Name "> <mx:TextInput id=" usernameInput " text="{selectedUser.username}"/> </mx:FormItem>
<mx:FormItem
label=" Password "> <mx:TextInput id=" passwordInput " text="{selectedUser.password}" displayAsPassword="true"/> </mx:FormItem>
<mx:FormItem
label=" Password "> <mx:TextInput id=" secondPasswordInput " text="" displayAsPassword="true"/> </mx:FormItem>
</mx:Form>
<mx:HBox
width=" 100% "> <mx:Button label=" New User " click="{resetForm()}"/> <mx:Button label=" Update User " click="{manager.update(selectedUser);resetForm()}"/> <mx:Button label=" Remove User " click="{manager.remove(selectedUser);resetForm()}"/> </mx:HBox>
<mx:Button
label=" Close " click="PopUpManager.removePopUp(this)"/> </mx:VBox>
</mx:TitleWindow>

每当单击列表中的项目时,就会从服务器检索关联的用户对象并将其存储在窗口的“ selectedUser”属性中。 此属性在MXML中定义,因此可以更轻松地与表单中的字段结合使用。 “ selectedUser”属性的属性和表单上的输入字段是“双向”彼此绑定的,因此“ selectedUser”属性的值更改(通过来自服务器的事件)反映在输入中字段和字段值的更改(通过用户输入)反映在“ selectedUser”属性的值中。 窗口上的按钮使用'selectedUser'属性作为参数链接到管理器上的方法。 方法调用的结果将反映在管理器维护的列表和窗口的列表中,因为它已绑定到该列表。

备注

重要的是要注意,使用此软件包,您将在系统中的某种类型的所有对象的客户端维护一个列表。 对于某些参考数据和您希望其实例数量有限的数据,可以这样做。 对于其他类型的数据,维护完整列表可能不是必需的甚至是不可行的。 在这些情况下,可以将相同的原理应用于完整列表的子集。

有趣的一点是,每当客户更改数据(保存,更新或删除域对象)时,他都会收到包含新列表的响应。 他还将收到所有其他用户都包含更新列表的消息。 因此,客户对其所做的每个更改都会收到两次更新。 可以删除第一个(对他的请求的响应),但已将其添加到系统中,因为直接响应通常比通过JMS的消息更及时。

值得一提的另一件事是,此模型中可能存在并发问题,因为包含更新的消息(在本例中为完整列表)通过不同的渠道传递。 邮件可能会延迟到达,而后来的邮件可能会早于它们到达。 这将意味着客户端正在显示陈旧数据。 解决此问题的一种方法是在消息中包含序列号,并在接收到消息时对照接收到的最新消息检查此序列号。

结论

通用软件包以易于使用的格式包装了前几章中找到的解决方案。

本文显示的解决方案为使用Flex和Grails开发JEE应用程序提供了坚实的基础。 使用这些工具箱进行JEE开发可以更快,更敏捷,并且也许最重要的是更有趣!

翻译自: https://www.infoq.com/articles/flex-grails/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

grails创建程序

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值