17、多数据库连接与DataSnap技术全解析

多数据库连接与DataSnap技术全解析

1. 本地数据库连接

在多层应用中,当客户端和服务器位于同一可执行文件时,合理利用提供者(providers)至关重要。为便于后续将服务器迁移到独立应用,建议将服务器端组件和客户端组件分别置于不同的数据模块中,应用结构如下:
- 服务器数据模块 :包含数据库连接、必要的数据集和提供者。
- 客户端数据模块 :包含客户端数据集和(可选)数据源。
- 窗体和单元 :应用中的窗体和支持单元将引用客户端数据模块中的数据。

除服务器数据模块内部外,不应引用服务器端的任何组件,所有数据访问都应通过客户端数据模块进行,这样能简化服务器数据模块迁移到独立应用的过程。

2. 使用不同窗体上的提供者

将数据访问组件和提供者移到单独的数据模块后,客户端数据集无法直接连接到数据集提供者。可通过调用 TClientDataSet.SetProvider 或使用 TLocalConnection 组件来解决此问题。在运行时,可使用以下代码建立连接:

ClientDataSet1.SetProvider(ServerDM.pvContacts);
3. TSQLClientDataSet组件

若使用dbExpress编写大量应用且不考虑轻松迁移到独立应用服务器, TSQLClientDataSet 组件可简化应用。它将 TSQLDataSet TDataSetProvider TClientDataSet 组件封装为单个组件。不过,若后续决定迁移到独立应用服务器,需在服务器端创建新的 TSQLDataSet TDataSetProvider 组件,并在客户端用 TClientDataSet 组件替换 TSQLClientDataSet 组件,且该组件会限制对单个提供者和数据集事件的控制。因此,不建议在大多数应用中使用 TSQLClientDataSet

4. 限制服务器返回的数据量

默认情况下,打开客户端数据集时,服务器端数据集返回的所有数据都会返回给客户端应用。但在某些情况下,可能需要限制一次返回的记录数。可通过设置 TClientDataSet PacketRecords 属性来实现:
- 默认值为 -1,表示服务器应一次性返回所有记录。
- 设置为大于零的数字时,确定服务器在给定时间内返回的最大记录数。
- 设置为零时,服务器仅返回元数据信息,不返回实际行数据。

通常,滚动客户端数据集时,每个数据包会自动从服务器获取。若将 TClientDataSet.FetchOnDemand 设置为 False ,则需手动调用 TClientDataSet.GetNextPacket 来获取下一个数据包。

5. 手动获取BLOB数据

为限制从服务器发送到客户端的数据量,可手动获取BLOB数据。默认情况下,提供者会随其他数据包一起返回BLOB数据。若并非总是需要BLOB信息,可开启 TDataSetProvider poFetchBlobsOnDemand 选项,并将 TClientDataSet.FetchOnDemand 设置为 False 。若客户端应用完全不需要BLOB数据,可更改 TSQLDataset CommandText 属性,使 SELECT 语句不检索数据库中的BLOB字段。

当设置 poFetchBlobsOnDemand 选项后,BLOB数据不会返回给客户端数据集。若尝试访问客户端数据集中的BLOB字段,将引发异常。若客户端需要当前记录的BLOB信息,可调用客户端数据集的 FetchBlobs 方法:

ClientDataSet1.FetchBlobs;

FetchBlobs 仅检索当前记录的所有BLOB字段。

6. 手动获取详细记录

另一种限制服务器返回数据量的方法是手动获取主/详细关系中的详细记录。默认情况下,所有详细记录(以嵌套数据集的形式)会随主记录一起发送到客户端。若并非总是需要详细信息,可开启 TDataSetProvider poFetchDetailsOnDemand 选项。

当设置 poFetchDetailsOnDemand 选项后,详细数据不会返回给客户端数据集。若客户端需要当前记录的详细信息,可调用客户端数据集的 FetchDetails 方法:

ClientDataSet1.FetchDetails;

同样,需将 TClientDataSet.FetchOnDemand 设置为 False

以下是一个示例应用,展示了如何使用 FetchBlobs FetchDetails 限制服务器返回的数据量:

unit MainForm;

interface

uses
  SysUtils, Types, Classes, QGraphics, QControls, QForms, QDialogs,
  QStdCtrls, DBXpress, FMTBcd, Provider, SqlExpr, DB, DBClient, QGrids,
  QDBGrids, QDBCtrls;

type
  TfrmMain = class(TForm)
    cdsContacts: TClientDataSet;
    conn: TSQLConnection;
    sqlContacts: TSQLDataSet;
    pvContacts: TDataSetProvider;
    sqlTodos: TSQLDataSet;
    dsContacts: TDataSource;
    dsClientContacts: TDataSource;
    gridContacts: TDBGrid;
    btnFetchBlobs: TButton;
    btnFetchDetails: TButton;
    cdsTodos: TClientDataSet;
    cdsContactsID: TIntegerField;
    cdsContactsFIRST: TStringField;
    cdsContactsLAST: TStringField;
    cdsContactsDEAR: TStringField;
    cdsContactsTITLE: TStringField;
    cdsContactsCOMPANYNAME: TStringField;
    cdsContactsADDRESS1: TStringField;
    cdsContactsADDRESS2: TStringField;
    cdsContactsCITY: TStringField;
    cdsContactsSTATE: TStringField;
    cdsContactsPOSTALCODE: TStringField;
    cdsContactsCOUNTRY: TStringField;
    cdsContactsPHONE: TStringField;
    cdsContactsFAX: TStringField;
    cdsContactsCELLULAR: TStringField;
    cdsContactsPAGER: TStringField;
    cdsContactsEMAIL: TStringField;
    cdsContactsIMAGE: TBlobField;
    cdsContactsNOTES: TMemoField;
    cdsContactssqlTodos: TDataSetField;
    lblDetails: TLabel;
    lblBlobs: TLabel;
    procedure FormCreate(Sender: TObject);
    procedure btnFetchDetailsClick(Sender: TObject);
    procedure btnFetchBlobsClick(Sender: TObject);
    procedure cdsContactsAfterScroll(DataSet: TDataSet);
  private
    procedure ShowTodoCount;
    { Private declarations }
  public
    { Public declarations }
  end;

var
  frmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);
begin
  cdsContacts.Open;
end;

procedure TfrmMain.btnFetchDetailsClick(Sender: TObject);
begin
  cdsContacts.FetchDetails;
  ShowTodoCount;
end;

procedure TfrmMain.btnFetchBlobsClick(Sender: TObject);
begin
  cdsContacts.FetchBlobs;
  ShowTodoCount;
end;

procedure TfrmMain.cdsContactsAfterScroll(DataSet: TDataSet);
begin
  ShowTodoCount;
end;

procedure TfrmMain.ShowTodoCount;
var
TestStream: TStream;
begin
  try
    TestStream := cdsContacts.CreateBlobStream(cdsContactsNOTES, bmRead);
    TestStream.Free;
    lblBLOBs.Caption := 'BLOBs have been fetched for this record';
  except
    lblBLOBs.Caption := 'BLOBs have not been fetched for this record';
  end;
  case cdsTodos.RecordCount of
    -MaxInt - 1:
      lblDetails.Caption := 'Todos have not been fetched for this record';
    1:
      lblDetails.Caption := '1 todo has been fetched for this record';
    else
      lblDetails.Caption := IntToStr(cdsTodos.RecordCount) +
        ' todos have been fetched for this record';
  end;
end;

end.
7. DataSnap技术概述

DataSnap是一种允许客户端应用连接到应用服务器中提供者的技术,由多个组件实现,可通过套接字、DCOM、CORBA、HTTP和SOAP等底层技术连接不同机器。在早期的Delphi版本中,DataSnap被称为MIDAS,由于国际商标考虑,名称已更改为DataSnap。

8. 创建应用服务器

创建应用服务器时,首先要设置一个或多个远程数据模块。远程数据模块是可从客户端应用远程访问的数据模块,包含之前章节中放置在服务器端数据模块上的组件。

在许多应用服务器中,单个远程数据模块就足够了,但也可根据需要创建多个远程数据模块,原因如下:
- 应用服务器需要连接多个数据库,为每个数据库连接创建一个不同的远程数据模块。
- 应用有两种类型的用户(如普通用户和管理员),可创建两个单独的远程数据模块,管理员使用的模块包含敏感数据的额外表或查询。
- 一个数据模块用于提供对底层数据库的访问,另一个数据模块用于提供其他非数据相关的服务,如数值计算。

创建远程数据模块的步骤因类型而异:
- 标准远程数据模块 :适用于通过套接字、DCOM或HTTP连接的客户端。创建步骤如下:
1. 创建一个新应用作为应用服务器。
2. 从Delphi主菜单中选择“文件” -> “新建” -> “其他”,选择“多层”选项卡。
3. 选择“远程数据模块”并点击“确定”。
4. 输入数据模块的CoClass名称,选择实例化类型和线程模型,通常实例化类型选择“Multiple Instance”。

实例化类型 描述
Internal 远程数据模块不能从外部客户端创建,总是在服务器应用内部创建
Single Instance 每个尝试连接到服务器的客户端将导致服务器可执行文件的一个单独实例运行
Multiple Instance 只有一个服务器可执行文件副本运行,但会为每个连接的客户端实例化一个单独的远程数据模块
线程模型 描述
Single 数据模块一次只接收来自单个客户端的请求,无需处理线程问题
Apartment 每个数据模块实例一次只处理一个请求,但服务器可同时处理多个数据模块上的多个请求,需要处理全局数据的多线程问题
Free 数据模块可同时接收来自多个客户端的多个请求,除处理全局数据的线程冲突外,还必须保护实例数据
Both 与Free相同,除了对客户端接口的任何回调是序列化的
Neutral 多个客户端可同时向同一数据模块发出请求,但COM确保没有两个请求相互冲突,仅在COM+下支持,非COM+下与Apartment模型相同
  • MTS远程数据模块 :适用于安装在MTS或COM+下的应用服务器,只能在ActiveX库中创建。创建步骤如下:
    1. 从Delphi主菜单中选择“文件” -> “新建” -> “其他”,选择“多层”选项卡。
    2. 选择“事务性数据模块”并点击“确定”。
    3. 输入数据模块的CoClass名称,选择线程模型和事务模型。

  • CORBA远程数据模块 :用于通过CORBA连接为客户端提供数据的应用服务器。创建步骤如下:
    1. 从Delphi主菜单中选择“文件” -> “新建” -> “其他”,选择“多层”选项卡。
    2. 选择“CORBA数据模块”并点击“确定”。
    3. 输入数据模块的CoClass名称,选择实例化类型和线程模型。

  • SOAP远程数据模块 :用于为设置为从Web服务访问数据的客户端提供数据。创建步骤如下:
    1. 从Delphi主菜单中选择“文件” -> “新建” -> “其他”,选择“Web服务”选项卡。
    2. 选择“SOAP服务器应用程序”并点击“确定”,选择SOAP服务器应用程序类型,若创建Web应用调试器可执行文件,输入COM对象的CoClass名称。
    3. 再次选择“文件” -> “新建” -> “其他”,选择“Web服务”选项卡。
    4. 选择“SOAP服务器数据模块”并点击“确定”,输入数据模块的类名。

9. 放置组件到远程数据模块

无论创建哪种远程数据模块,都需用组件填充它。通常,远程数据模块上的组件与之前章节中服务器端数据模块上的组件相同,即数据库连接组件、数据集和提供者。由于使用dbExpress,需用 TSQLConnection 、一个或多个 TSQLDataSets 和一个或多个 TDataSetProvider 组件填充数据模块。

10. 向远程数据模块添加方法

除了以数据集的形式提供数据,远程数据模块还可为客户端应用提供其他服务。添加方法的步骤如下:
1. 在远程数据模块的源代码编辑器中右键单击,选择“添加到接口”菜单项。
2. 在“声明”编辑框中输入新方法调用的声明,点击“确定”。
3. 填充方法的具体实现。

例如,创建一个简单的 GetServerTime 方法:

function TMethodsDM.GetServerTime: TDateTime;
begin
  Result := Now;
end;
11. 回调机制

服务器应用除了接收客户端的调用,还可使用回调接口调用客户端应用。创建回调接口的步骤如下:
1. 从Delphi主菜单中选择“视图” -> “类型库”,打开类型库编辑器。
2. 点击“新建接口”按钮,命名新接口(如 ITestCallback )。
3. 点击“新建方法”工具栏按钮,命名新方法(如 Test )。
4. 关闭类型库编辑器。

客户端需通过创建一个接受 ITestCallback 类型参数的服务器方法,并将其值保存在远程数据模块的局部变量中来传递回调接口:

type
  TMethodsDM = class(TRemoteDataModule, IMethodsDM)
    ...
  private
    { Private declarations }
    FCallback: ITestCallback;
    ...
  end;

...

procedure TMethodsDM.SetCallback(const Callback: ITestCallback);
begin
  FCallback := Callback;
end;

回调机制有一些限制,使用套接字连接时,需将 TSocketConnection.SupportsCallbacks 设置为 True ,且回调接口必须派生自 IDispatch TWebConnection TSOAPConnection 不支持回调。

12. 创建应用服务器的用户界面

应用服务器通常运行在专用服务器上,一般不需要太多用户界面。但为了显示登录用户数量、数据库统计信息等相关信息,提供一个最小的用户界面是有用的。创建界面的方法是在数据模块的 OnCreate OnDestroy 事件处理程序中通知主窗体。

以下是一个示例代码:

unit MainForm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

const
  UM_CONNECT = WM_USER + 1;

type
  TfrmMain = class(TForm)
    lblConnections: TLabel;
  private
    { Private declarations }
    FConnections: Integer;
    procedure UpdateConnections;
    procedure UMConnect(var Msg: TMessage); message UM_CONNECT;
  public
    { Public declarations }
  end;

var
  frmMain: TfrmMain;

implementation

{$R *.dfm}

{ TfrmMain }

procedure TfrmMain.UMConnect(var Msg: TMessage);
begin
  FConnections := FConnections + Msg.WParam;
  UpdateConnections;
end;

procedure TfrmMain.UpdateConnections;
begin
  if FConnections = 1 then
    lblConnections.Caption := '1 connection'
  else
    lblConnections.Caption := IntToStr(FConnections) + ' connections';
end;

end.

远程数据模块在创建和销毁时调用主窗体的方法:

procedure TMethodsDM.RemoteDataModuleCreate(Sender: TObject);
begin
  PostMessage(frmMain.Handle, UM_CONNECT, 1, 0); 
end;

procedure TMethodsDM.RemoteDataModuleDestroy(Sender: TObject);
begin
  PostMessage(frmMain.Handle, UM_CONNECT, -1, 0);
end;
13. 准备应用服务器进行测试

完成应用服务器的编写后,应将其安装到服务器机器上并注册进行测试。建议先在开发机器上进行测试,原因如下:
- 开始创建应用服务器时可能会犯很多错误,需要频繁修改、重新编译和重新部署应用服务器,在本地开发机器上操作更方便。
- 应用服务器在调试前可能容易挂起或崩溃,在开发机器上崩溃比在生产服务器上崩溃更好。

多数据库连接与DataSnap技术全解析

14. 创建客户端应用

创建客户端应用时,主要任务是建立与应用服务器的连接,并使用服务器提供的数据和服务。以下是创建客户端应用的一般步骤:
1. 选择连接组件 :根据应用服务器使用的底层技术,选择合适的连接组件,如 TSocketConnection (套接字)、 TDCOMConnection (DCOM)、 THTTPRIO (HTTP)或 TSOAPConnection (SOAP)。
2. 配置连接属性 :设置连接组件的属性,如服务器地址、端口号、远程数据模块的名称等。
3. 连接到服务器 :在客户端应用中调用连接组件的 Connected 属性或 Connect 方法,建立与应用服务器的连接。
4. 访问服务器数据和服务 :通过连接组件访问远程数据模块提供的数据集和方法。

以下是一个简单的客户端应用示例,使用 TDCOMConnection 连接到应用服务器,并调用远程数据模块的 GetServerTime 方法:

unit MainForm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, DBClient, DB, DCOMConnect;

type
  TfrmMain = class(TForm)
    btnGetServerTime: TButton;
    lblServerTime: TLabel;
    dcomConnection: TDCOMConnection;
    procedure btnGetServerTimeClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  frmMain: TfrmMain;

implementation

{$R *.dfm}

procedure TfrmMain.btnGetServerTimeClick(Sender: TObject);
var
  ServerTime: TDateTime;
begin
  if dcomConnection.Connected then
  begin
    ServerTime := dcomConnection.AppServer.GetServerTime;
    lblServerTime.Caption := DateTimeToStr(ServerTime);
  end;
end;

end.
15. 完整示例:Methods应用

下面通过一个完整的示例 Methods 来展示如何创建一个包含服务器和客户端的多层应用。

服务器端

服务器端包括一个远程数据模块 TMethodsDM 和一个主窗体 TfrmMain 。远程数据模块提供了 GetServerTime 方法和回调机制,主窗体显示当前连接的客户端数量。

// 远程数据模块 MethodsDM.pas
unit MethodsDM;

interface

uses
  SysUtils, Classes, DB, DBClient, Provider, SqlExpr, DBXpress;

type
  TMethodsDM = class(TRemoteDataModule, IMethodsDM)
    sqlConnection: TSQLConnection;
    sqlDataSet: TSQLDataSet;
    dataSetProvider: TDataSetProvider;
    procedure RemoteDataModuleCreate(Sender: TObject);
    procedure RemoteDataModuleDestroy(Sender: TObject);
  private
    { Private declarations }
    FCallback: ITestCallback;
    function GetServerTime: TDateTime; safecall;
    procedure SetCallback(const Callback: ITestCallback); safecall;
  public
    { Public declarations }
  end;

implementation

{$R *.dfm}

function TMethodsDM.GetServerTime: TDateTime;
begin
  Result := Now;
end;

procedure TMethodsDM.SetCallback(const Callback: ITestCallback);
begin
  FCallback := Callback;
end;

procedure TMethodsDM.RemoteDataModuleCreate(Sender: TObject);
begin
  PostMessage(frmMain.Handle, UM_CONNECT, 1, 0);
end;

procedure TMethodsDM.RemoteDataModuleDestroy(Sender: TObject);
begin
  PostMessage(frmMain.Handle, UM_CONNECT, -1, 0);
end;

end.

// 主窗体 MainForm.pas
unit MainForm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

const
  UM_CONNECT = WM_USER + 1;

type
  TfrmMain = class(TForm)
    lblConnections: TLabel;
  private
    { Private declarations }
    FConnections: Integer;
    procedure UpdateConnections;
    procedure UMConnect(var Msg: TMessage); message UM_CONNECT;
  public
    { Public declarations }
  end;

var
  frmMain: TfrmMain;

implementation

{$R *.dfm}

{ TfrmMain }

procedure TfrmMain.UMConnect(var Msg: TMessage);
begin
  FConnections := FConnections + Msg.WParam;
  UpdateConnections;
end;

procedure TfrmMain.UpdateConnections;
begin
  if FConnections = 1 then
    lblConnections.Caption := '1 connection'
  else
    lblConnections.Caption := IntToStr(FConnections) + ' connections';
end;

end.
客户端

客户端包括一个主窗体 TfrmMain ,使用 TDCOMConnection 连接到服务器,并调用 GetServerTime 方法和设置回调接口。

unit MainForm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, DBClient, DB, DCOMConnect;

type
  TfrmMain = class(TForm)
    btnGetServerTime: TButton;
    lblServerTime: TLabel;
    dcomConnection: TDCOMConnection;
    btnSetCallback: TButton;
    procedure btnGetServerTimeClick(Sender: TObject);
    procedure btnSetCallbackClick(Sender: TObject);
  private
    { Private declarations }
    function Test: Integer; safecall;
  public
    { Public declarations }
  end;

var
  frmMain: TfrmMain;

implementation

{$R *.dfm}

procedure TfrmMain.btnGetServerTimeClick(Sender: TObject);
var
  ServerTime: TDateTime;
begin
  if dcomConnection.Connected then
  begin
    ServerTime := dcomConnection.AppServer.GetServerTime;
    lblServerTime.Caption := DateTimeToStr(ServerTime);
  end;
end;

procedure TfrmMain.btnSetCallbackClick(Sender: TObject);
begin
  if dcomConnection.Connected then
  begin
    dcomConnection.AppServer.SetCallback(Self as ITestCallback);
  end;
end;

function TfrmMain.Test: Integer;
begin
  ShowMessage('Callback received from server!');
  Result := 0;
end;

end.
16. 公文包模型

公文包模型是一种离线数据访问模式,允许客户端在离线状态下访问和修改数据,然后在重新连接到服务器时将更改同步回服务器。在公文包模型中,客户端应用使用 TClientDataSet 缓存服务器数据,并在离线时进行操作。以下是使用公文包模型的一般步骤:
1. 下载数据 :在客户端应用连接到服务器时,将服务器数据下载到 TClientDataSet 中。
2. 离线操作 :客户端应用在离线状态下对 TClientDataSet 中的数据进行增删改操作。
3. 同步数据 :当客户端应用重新连接到服务器时,将 TClientDataSet 中的更改同步回服务器。

graph LR
    A[连接服务器] --> B[下载数据到TClientDataSet]
    B --> C[离线操作数据]
    C --> D[重新连接服务器]
    D --> E[同步更改到服务器]
17. 无状态服务器

无状态服务器是指服务器不保存客户端的会话状态,每个请求都是独立的。使用无状态服务器的优点是可扩展性和容错性更好,但需要客户端在每个请求中提供必要的上下文信息。在多层应用中,可通过以下方式实现无状态服务器:
- 使用会话标识 :客户端在每个请求中包含会话标识,服务器根据会话标识识别客户端。
- 传递必要的上下文信息 :客户端在每个请求中传递必要的上下文信息,如用户身份、请求参数等。

18. 多个客户端数据集共享连接

在某些情况下,多个客户端数据集可能需要共享同一个数据库连接,以提高性能和资源利用率。以下是实现多个客户端数据集共享连接的步骤:
1. 创建一个共享的数据库连接组件 :如 TSQLConnection
2. 将共享连接组件的引用传递给多个客户端数据集 :在客户端数据集中设置 SQLConnection 属性为共享连接组件。

// 创建共享连接组件
var
  SharedConnection: TSQLConnection;

// 在客户端数据集中使用共享连接
cdsContacts.SQLConnection := SharedConnection;
cdsTodos.SQLConnection := SharedConnection;
19. 多个服务器之间的连接代理

在复杂的多层应用中,可能需要在多个服务器之间进行连接代理,以实现负载均衡、故障转移等功能。连接代理可通过中间件或代理服务器实现,以下是一个简单的连接代理流程:

graph LR
    A[客户端请求] --> B[代理服务器]
    B --> C{选择服务器}
    C -->|服务器1| D[服务器1处理请求]
    C -->|服务器2| E[服务器2处理请求]
    D --> F[返回结果给代理服务器]
    E --> F
    F --> A[返回结果给客户端]
20. 总结

通过上述内容,我们详细介绍了多层应用中本地数据库连接、DataSnap技术、应用服务器和客户端应用的创建方法,以及公文包模型、无状态服务器、多个客户端数据集共享连接和多个服务器之间的连接代理等高级主题。在实际开发中,可根据具体需求选择合适的技术和方法,以构建高效、可扩展和稳定的多层数据库应用。

在创建多层应用时,需要注意以下几点:
- 合理设计应用结构 :将服务器端和客户端组件分离,便于维护和扩展。
- 注意数据安全 :在传输和存储数据时,采取必要的安全措施,如加密、身份验证等。
- 优化性能 :通过限制服务器返回的数据量、使用缓存等方法,提高应用的性能。
- 处理错误和异常 :在客户端和服务器端都要处理可能出现的错误和异常,确保应用的稳定性。

希望这些内容能帮助你更好地理解和应用多层数据库开发技术。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值