Custom DFS service development

本文提供了一个简单的DFS服务开发教程,包括服务创建、Java客户端创建及.NET远程客户端的实现。通过逐步指南介绍了如何从零开始搭建并部署DFS服务,并演示了不同平台上的远程调用过程。

原始文章:http://www.dfsnotes.com/2007/11/14/custom-dfs-service-development-service-creation-part-1/

Some persons from EMC Documentum Forums stated that AcmeCustomService which comes with DFS SDK seems to be too complex for beginners, and “HelloWorld” service would be more understandable. I will try to show you how to create your first own simple HelloWorld like DFS service.

This tutorial will contain of 3 parts:

  • Service creation
  • Java client creation
  • .NET remote client

Before we’ll start, we should setup our build environment.

Note: You should have ant installed in order to run tutorial scripts.

1. Download attached helloworldservice.zip and extract it into emc-dfs-sdk-6.0\samples folder. It contains project template similar to AcmeCustomService. Lets assume “HelloWorldService” folder as root.

2. Open etc\dfc.properties and set host, repository name, username and password for your docbase.

3. Open build.properties and set preferred context root and module names, or leave them as they’re now.

Here is service code:

package com.service.example;

import com.emc.documentum.fs.rt.annotations.DfsPojoService;

@DfsPojoService(targetNamespace = “http://example.service.com”, requiresAuthentication = true)
public class HelloWorldService
{
public String sayHello(String name)
{
return “Hello ” + name;
}
}

@DfsPojoService annotation specifies service’s namespace, and states if service requires docbase authentication (true by default)

Place this source with appropriate package into src\service folder.

Go to root folder (HelloWorldService) and run from command line the following command:

ant artifacts package

This command converts annotated classes to DFS services and package them into ear file.

At this step, you should have example.ear deployment file in HelloWorldService\build folder. This ear file contains our service which can be deployed into Weblogic application server.

Copy this file into weblogic’s autodeploy directory. You can also set weblogic deployment dir at autodeploy.properties file and run “ant deploy” command.

With default values at build.properties, your service should be available by the path: http://host:port/services/example/HelloWorldService

That’s all for now. In the next part of tutorial I will explain how to create client and call this service remotely.

 

Today we’ll talk about remote client creation which invokes our HelloWorldService from part 1.

Little more info about this sample:

 

  • Before running remote client, HelloWorldService should be built and packaged.
  • Since HelloWorldService requires authentication, the service context should be provided with at least one identity. A service context is expected to contain only one identity per repository name.
  • We invoke HelloWorldService remotely, and therefore we should provide client with connection details. This can be done through two ways:
    1. Using dfs-client.xml config file: You should have it under “resources/config” folder.
    2. Using overrided methods of ContextFactory and ServiceFactory factories, it allows to specify explicit location of service:
      IServiceContext registeredContext = ContextFactory.getInstance().register(context, moduleName, contextRoot);
      ServiceFactory.getInstance().getRemoteService(IHelloWorldService.class, registeredContext, moduleName, contextRoot);

Most common scheme of creating and invoking authorized service remotely consists of:

  1. Create service context.
  2. Fill it with identities.
  3. Register context at Context Registry Service and get “clean” context.
  4. Get required service by implemented interface.
  5. Invoke service’s method.

Below is full source code of remote client:

package com.client.example;

import com.emc.documentum.fs.datamodel.core.context.RepositoryIdentity;
import com.emc.documentum.fs.rt.ServiceException;
import com.emc.documentum.fs.rt.context.ContextFactory;
import com.emc.documentum.fs.rt.context.IServiceContext;
import com.emc.documentum.fs.rt.context.ServiceFactory;
import com.service.example.client.IHelloWorldService;

public class HelloWorldClient
{
public static void main(String[] args)
{
String moduleName = “example”;
String contextRoot = “http://localhost:7001/services/”;

ContextFactory contextFactory = ContextFactory.getInstance();
IServiceContext context = contextFactory.newContext();

RepositoryIdentity repoId = new RepositoryIdentity();

repoId.setRepositoryName(”repositoryName”);
repoId.setUserName(”userName”);
repoId.setPassword(”password”);
repoId.setDomain(”");

context.addIdentity(repoId);

try
{
IServiceContext registeredContext = contextFactory.register(context, moduleName, contextRoot);
ServiceFactory serviceFactory = ServiceFactory.getInstance();
IHelloWorldService service = serviceFactory.getRemoteService(IHelloWorldService.class, registeredContext, moduleName, contextRoot);
String response = service.sayHello(”John”);
System.out.println(”response = ” + response);
}
catch (ServiceException e)
{
e.printStackTrace();
}
}
}

I’ve updated helloworldservice.zip with remote client source code.

Next part of .NET remote client will be ready soon.

 

It’s time to show you how to create .NET remote client for HelloWorldService.

In order to create .NET proxies for DFS service, developers should have:

  • Service’s WSDL URL.
  • DFS service-model.xml file which describes service artifacts to be generated by subsequent processes.

 

The data above is required for DFS Proxy Generator tool which creates .NET DFS proxy:

DFS Proxy Generator

After creation of proxy, it can be used for service instantiation and invocation.

Below is source code which creates and invokes HelloWorldService. As I said in my previous post DFS 6.0 SP1 is coming, Java and .NET DFS API’s are very similar to each other, there is no conceptual differences between them.

Source code:

using System;
using Client.Service;
using Emc.Documentum.FS.DataModel.Core.Context;
using Emc.Documentum.FS.Runtime.Context;

namespace Client
{
class Program
{
static void Main(string[] args)
{
string moduleName = “example”;
string contextRoot = “http://localhost:7001/services”;

ContextFactory contextFactory = ContextFactory.Instance;
IServiceContext context = contextFactory.NewContext();
RepositoryIdentity repoId = new RepositoryIdentity();
repoId.RepositoryName = “yourreponame”;
repoId.UserName = “yourusername”;
repoId.Password = “yourpwd”;

context.AddIdentity(repoId);

// Remote service invocation
IHelloWorldService mySvc;
try
{
context = contextFactory.Register(context, moduleName, contextRoot);
ServiceFactory serviceFactory = ServiceFactory.Instance;
mySvc = serviceFactory.GetRemoteService<IHelloWorldService>(context, moduleName, contextRoot);
string response = mySvc.SayHello(”John”);
Console.WriteLine(”response = ” + response);
}
catch (Exception e)
{
Console.Error.WriteLine(e.StackTrace);
}
}
}
}

This sample is also included in helloworldservice.zip.

 

P.S: Please note, that DFS SP1 is required in order to build this sample.

using Blazored.LocalStorage; using DocumentFormat.OpenXml.ExtendedProperties; using DocumentFormat.OpenXml.Math; using log4net; using log4net.Config; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using Org.BouncyCastle.Crypto.Engines; using Radzen; using Radzen.Blazor.Rendering; using SqlSugar.IOC; using System.IdentityModel.Tokens.Jwt; using System.Reflection; using System.Runtime.InteropServices; using System.Text; using Tewr.Blazor.FileReader; using WebApi.AgvServer.Api; using WebApi.AgvServer.Auth; using WebApi.AgvServer.Handles; using WebApi.AgvServer.Services; using WebApi.AgvServer.TimerService; using WebApi.AgvServer.Utility; using WebApi.AgvServer.Utility._AutoMapper; using WebApi.Common.Helper; using WebApi.Common.LicenceHelper; using WebApi.Common.Logger; using WebApi.IRepository; using WebApi.IRepository.Agvs; using WebApi.IRepository.Path; using WebApi.IRepository.Stations; using WebApi.IRepository.Tasks; using WebApi.IRepository.Users; using WebApi.IService; using WebApi.IService.AgvCommunication; using WebApi.IService.Agvs; using WebApi.IService.External; using WebApi.IService.Path; using WebApi.IService.Stations; using WebApi.IService.Tasks; using WebApi.IService.Users; using WebApi.Model.Common; using WebApi.Repository; using WebApi.Repository.Agvs; using WebApi.Repository.Path; using WebApi.Repository.Stations; using WebApi.Repository.Tasks; using WebApi.Repository.Users; using WebApi.Service; using WebApi.Service.AgvCommunication; using WebApi.Service.Agvs; using WebApi.Service.External; using WebApi.Service.Path; using WebApi.Service.Stations; using WebApi.Service.Tasks; using WebApi.Service.Users; using System.Net.Http; #region ���ÿ��ٱ༭ģʽ����windowsϵͳ��ע�͵� const int STD_INPUT_HANDLE = -10; const uint ENABLE_QUICK_EDIT_MODE = 0x0040; const uint ENABLE_INSERT_MODE = 0x0020; [DllImport("kernel32.dll", SetLastError = true)] static extern IntPtr GetStdHandle(int hConsoleHandle); [DllImport("kernel32.dll", SetLastError = true)] static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint mode); [DllImport("kernel32.dll", SetLastError = true)] static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint mode); #endregion #region ���ÿ��ٱ༭ģʽ����windowsϵͳ��ע�͵� IntPtr hStdin = GetStdHandle(STD_INPUT_HANDLE); uint mode; GetConsoleMode(hStdin, out mode); mode &= ~ENABLE_QUICK_EDIT_MODE;//�Ƴ����ٱ༭ģʽ mode &= ~ENABLE_INSERT_MODE; //�Ƴ�����ģʽ SetConsoleMode(hStdin, mode); #endregion #region �����windowsϵͳ�����ÿ���̨���ڱ��� [DllImport("kernel32.dll", SetLastError = true)] static extern bool SetConsoleTitle(string lpConsoleTitle); // ���ÿ���̨���ڱ��� SetConsoleTitle("AGV���ȹ���ϵͳ"); #endregion var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); builder.Services.AddHttpContextAccessor(); builder.Configuration.AddJsonFile("appsettings.json"); // 注册 HttpClientFactory builder.Services.AddHttpClient(); #region Swaggerע�� builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApi.AgvServer", Version = "v1" }); #region SwaggerGen ��Ȩ��� c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { In = ParameterLocation.Header, Type = SecuritySchemeType.ApiKey, Description = "ֱ��������������Bearer {token}(ע���м��пո�)", Name = "Authorization", BearerFormat = "JWT", Scheme = "Bearer" }); c.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference= new OpenApiReference { Type=ReferenceType.SecurityScheme, Id="Bearer" } }, new string[]{ } } }); #endregion }); #endregion #region SqlSugar IOC var sqlInfo = builder.Configuration.GetSection(nameof(SQLInfo)).Get<SQLInfo>(); if (sqlInfo != null) { #region �������� //MySql = 0, //SqlServer = 1, //Sqlite = 2, //Oracle = 3, //PostgreSQL = 4, //Dm = 5, //Kdbndp = 6, //Oscar = 7, //MySqlConnector = 8, //Access = 9, //OpenGauss = 10, //QuestDB = 11, //HG = 12, //ClickHouse = 13, //GBase = 14, //Odbc = 15, //OceanBaseForOracle = 16, //TDengine = 17, //GaussDB = 18, //OceanBase = 19, //Tidb = 20, //Vastbase = 21, //PolarDB = 22, //Doris = 23, //Xugu = 24, //GoldenDB = 25, //TDSQLForPGODBC = 26, //TDSQL = 27, //HANA = 28, //DB2 = 29, //GaussDBNative = 30, //DuckDB = 31, //MongoDb = 32, //Custom = 900 #endregion builder.Services.AddSqlSugar(new IocConfig() { ConnectionString = sqlInfo.ConnStr, DbType = sqlInfo.SQLType, IsAutoCloseConnection = true,//�Զ��ͷ� }); #region �������� //services.ConfigurationSugar(db => //{ // db.CurrentConnectionConfig.ConfigureExternalServices = new ConfigureExternalServices() // {//�������� // DataInfoCacheService = myCache, // }; // db.CurrentConnectionConfig.MoreSettings = new ConnMoreSettings() // { // IsAutoRemoveDataCache = true // }; //}); #endregion } #endregion #region ��֤ //��֤ע�� builder.Services.AddScoped<ImitateAuthStateProvider>(); builder.Services.AddScoped<AuthenticationStateProvider>(implementationFactory => implementationFactory.GetRequiredService<ImitateAuthStateProvider>()); //jwt builder.Services.AddScoped<JwtSecurityTokenHandler>(); #region httpclientע�� builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri("http://localhost:9000") }); builder.Services.AddScoped<IAuthService, AuthService>(); builder.Services.AddScoped<AuthController>(); builder.Services.AddScoped<agvController>(); builder.Services.AddBlazoredLocalStorage(config => config.JsonSerializerOptions.WriteIndented = true); #endregion //jwt��֤ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => { //ȡ��˽Կ var secretByte = Encoding.UTF8.GetBytes(builder.Configuration["Authentication:SecretKey"] ?? "Blazor!SecKey@okagv@vvip.GANLIHUA2025"); options.TokenValidationParameters = new TokenValidationParameters() { //��֤������ ValidateIssuer = true, ValidIssuer = builder.Configuration["Authentication:Issuer"], //��֤������ ValidateAudience = true, ValidAudience = builder.Configuration["Authentication:Audience"], //��֤�Ƿ���� ValidateLifetime = true, //��֤˽Կ IssuerSigningKey = new SymmetricSecurityKey(secretByte) }; }); //jwt��չ builder.Services.AddScoped<IJWTHelper, JWTHelper>(); #endregion #region ��ȡ��Ŀ���� var projectName = builder.Configuration["Authentication:Audience"]; if (projectName == null) { projectName = "Test"; } #endregion #region Radzenע�� //Radzen builder.Services.AddRadzenComponents(); builder.Services.AddFileReaderService(); builder.Services.AddRadzenCookieThemeService(options => { options.Name = $"{projectName}RadzenBlazorTheme"; // The name of the cookie options.Duration = TimeSpan.FromDays(365); // The duration of the cookie }); //services.AddServerSideBlazor().AddHubOptions(o => //{ // o.MaximumReceiveMessageSize = 64 * 1024; //}); builder.Services.AddScoped<DialogService>(); builder.Services.AddScoped<NotificationService>(); builder.Services.AddScoped<ContextMenuService>(); #endregion #region IOC����ע�� builder.Services.AddCustomIOC(); #endregion #region JWT��Ȩ //builder.Services.AddCustomJWT(); #endregion #region AutoMapper builder.Services.AddAutoMapper(typeof(CustomAutoMapperProfile)); #endregion #region log4net builder.Services.AddControllers().AddControllersAsServices(); Log4netHelper.Repository = LogManager.CreateRepository("NETCoreRepository"); XmlConfigurator.Configure(Log4netHelper.Repository, new System.IO.FileInfo(System.IO.Path.GetDirectoryName(typeof(Program).Assembly.Location) + "/Config/log4net.config")); #endregion #region net logger var verstion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown"; builder.Logging.AddLocalFileLogger(options => { options.SaveDays = 60; options.Verstion = verstion; }); #endregion #region cookie builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme). AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, o => { o.LoginPath = new PathString("/Home/Login"); }); #endregion #region ��ȡappsetting���� try { //TaskInfo.IsAutoTask = ParamtersSettings.Load().IsAutoTask; //TaskInfo.IsAutoSendToAGV = ParamtersSettings.Load().IsAutoSendToNDC; //TaskInfo.LastKey = ParamtersSettings.Load().NncKey; //TaskInfo.TaskPause = ParamtersSettings.Load().TaskPause; //TaskInfo.SetTaskMode = ParamtersSettings.Load().SetTaskMode; //AGVMapInfo.mapInfo = ParamtersSettings.Load().mapInfo; //if (AGVMapInfo.mapInfo == null) //{ // AGVMapInfo.mapInfo = new AGVMapInfo(); // ParamtersSettings paramters = ParamtersSettings.Load(); // paramters.mapInfo = AGVMapInfo.mapInfo; // paramters.Save(); //} } catch { } #endregion #region ע��Licenceϵͳ��Ϣ�ļ� string computerFile = LicenceHelper.GetComputerInfoAndGenerateFile(); #endregion builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } //app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.MapControllers();//���ӿ��Ʋ����ս�� app.MapBlazorHub(); app.MapFallbackToPage("/_Host"); //app.MapFallbackToPage("/Login"); #region ���ػ��������� //const string CULTURE_CHINESE = "zh-CN"; //const string CULTURE_ENGLISTH = "en-US"; //const string CULTURE_RUSSIA = "ru-RU"; //app.UseRequestLocalization(options => //{ // var cultures = new[] { CULTURE_CHINESE, CULTURE_ENGLISTH, CULTURE_RUSSIA }; // options.AddSupportedCultures(cultures); // options.AddSupportedUICultures(cultures); // options.SetDefaultCulture(CULTURE_ENGLISTH); // // ��Http��Ӧʱ���� ��ǰ������Ϣ ���õ� Response Header��Content-Language �� // options.ApplyCurrentCultureToResponseHeaders = true; //}); #endregion app.UseAuthentication(); app.UseAuthorization(); app.Run(); #region �ⲿIO����ע�� public static class IOCExtend { /// <summary> /// ע���ⲿ���� /// </summary> /// <param name="services"></param> /// <returns></returns> public static IServiceCollection AddCustomIOC(this IServiceCollection services) { #region �Ż�SignalIR�����紫�� services.AddSignalR().AddJsonProtocol(options => { options.PayloadSerializerOptions.WriteIndented = false; // ����JSON��������С������� }); #endregion services.AddSingleton<CircuitHandler, CustomCircuitHandler>(); services.AddSingleton<IUserInfoRepository, UserInfoRepository>(); services.AddSingleton<IRoleInfoRepository, RoleInfoRepository>(); services.AddSingleton<IRightInfoRepository, RightInfoRepository>(); services.AddSingleton<IRoleRightMappingRepository, RoleRightMappingRepository>(); services.AddSingleton<IAgvInfoRepository, AgvInfoRepository>(); services.AddSingleton<IAgvConfigInfoRepository, AgvConfigInfoRepository>(); services.AddSingleton<IAgvErrorInfoRepository, AgvErrorInfoRepository>(); services.AddSingleton<IProcessInfoRepository, ProcessInfoRepository>(); services.AddSingleton<ITaskInfoRepository, TaskInfoRepository>(); services.AddSingleton<IAreaInfoRepository, AreaInfoRepository>(); services.AddSingleton<IStationInfoRepository, StationInfoRepository>(); services.AddSingleton<ILogInfoRepository, LogInfoRepository>(); services.AddSingleton<IControlAreaInfoRepository, ControlAreaInfoRepository>(); services.AddSingleton<IPointInfoRepository, PointInfoRepository>(); services.AddSingleton<ILineInfoRepository, LineInfoRepository>(); services.AddSingleton<IDTCInfoRepository, DTCInfoRepository>(); services.AddSingleton<IStationTypeInfoRespository, StationTypeInfoRepository>(); services.AddSingleton<IStationTypeReleInfoRepository, StationTypeReleInfoRepository>(); services.AddSingleton<ISystemConfigInfoRepository, SystemConfigInfoRepository>(); services.AddSingleton<IUserInfoService, UserInfoService>(); services.AddSingleton<IRoleInfoService, RoleInfoService>(); services.AddSingleton<IRightInfoService, RightInfoService>(); services.AddSingleton<IRoleRightMappingService, RoleRightMappingService>(); services.AddSingleton<IAgvInfoService, AgvInfoService>(); services.AddSingleton<IAgvConfigInfoService, AgvConfigInfoService>(); services.AddSingleton<IAgvErrorInfoService, AgvErrorInfoService>(); services.AddSingleton<IProcessInfoService, ProcessInfoService>(); services.AddSingleton<ITaskInfoService, TaskInfoService>(); services.AddSingleton<IAreaInfoService, AreaInfoService>(); services.AddSingleton<IStationInfoService, StationInfoService>(); services.AddSingleton<ILogInfoService, LogInfoService>(); services.AddSingleton<IModbusService, ModbusService>(); services.AddSingleton<IControlButtonOpenTcpService, ControlButtonOpenTcpService>(); services.AddSingleton<IMqttClientService, MqttClientService>(); services.AddSingleton<INDCService, NDCService>(); services.AddSingleton<IControlAreaInfoService, ControlAreaInfoService>(); services.AddSingleton<IPointInfoService, PointInfoService>(); services.AddSingleton<ILineInfoService, LineInfoService>(); services.AddSingleton<IOpenTcpService, OpenTcpService>(); services.AddSingleton<IDTCInfoService, DTCInfoService>(); services.AddSingleton<IStationTypeInfoService, StationTypeInfoService>(); services.AddSingleton<IStationTypeReleInfoService, StationTypeReleInfoService>(); services.AddSingleton<IStationCommunicationService, StationCommunicationService>(); services.AddSingleton<IWcsHttpService, WcsHttpService>(); services.AddSingleton<ISystemConfigInfoService, SystemConfigService>(); services.AddScoped<NorthwindService>(); //services.AddSingleton<ISplitterCommunicationService, SplitterCommunicationService>(); //TimerService services.AddHostedService<NdcGlobalParameterTimerService>(); services.AddHostedService<TaskIssuedTimerService>(); services.AddHostedService<TaskUpdateToSqlTimerService>(); services.AddHostedService<ProcessUpdateToSqlTimerService>(); services.AddHostedService<CreateTaskTimerService>(); services.AddHostedService<AreaUpdateToSqlTimerService>(); services.AddHostedService<StationUpdateToSqlTimerService>(); services.AddHostedService<TaskLeaveSyncTimerService>(); services.AddHostedService<TaskEnterSyncTimerService>(); services.AddHostedService<OpenTcpTimerService>(); services.AddHostedService<StationCommunicationTimerService>(); services.AddHostedService<AgvRunTimeCalculateTimerService>(); services.AddHostedService<TaskRequestToWMSTimerService>(); services.AddHostedService<NdcLocalModeModbusTimerService>(); services.AddHostedService<DataInitializationTimerService>(); services.AddHostedService<LicenceTimerService>(); // services.AddHostedService<SplitterCommunicationTimerService>(); return services; } public static IServiceCollection AddCustomJWT(this IServiceCollection services) { services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("SDMC-CJAS1-SAD-DFSFA-SADHJVF-VF")), ValidateIssuer = true, ValidIssuer = "http://localhost:6000", ValidateAudience = true, ValidAudience = "http://localhost:8080", ValidateLifetime = true, ClockSkew = TimeSpan.FromMinutes(value: 60) }; }); return services; } } #endregion 比如我这个代码要注册后再SplitterCommunicationService中使用该怎么注册
08-15
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值