【转】ASP.NET状态存储管理九大兵器

稍微经过整理,转自:http://aierong.cnblogs.com/archive/2004/07/14/23950.html

       网页状态是任何类型数据能够在一定时期内保持活跃的信息。我们这里说的一定时期可以是整个应用程序的生命周期,也可以是用户操作程序的时间,也可以是单个页面的生命周期等等。时间是有长有短的。

      由于WEB程序的HTTP协议是无状态的,所以存储状态信息就成了一个要解决的问题,既然要存储数据,那么存储的数据就需要有个存放位置,我认为只有2个地方:
·客户机
·服务器

按照存放位置进行分类,ASP。NET中状态存放方式如下:

·客户机
©查询字苻串---利用URL在客户机和服务器间进行数据交换
©隐藏的窗体字段---在窗体字段中设置和读取数据
©COOKIE---保存在客户浏览器上的数据
©视图状态---保存页面相关数据
·服务器
©应用程序---这种数据在应用程序整个生命周期内可以被所有用户利用
©会话---这种数据和每个用户联系
©暂存状态---这种数据在单个请求过程中存在
©缓存---这种数据与应用程序相类似
©其他物理数据存放媒体---例如数据库,TXT文本,XML文件等等

九大兵器之一——查询字苻串

查询字苻串是追加在URL后的数据(也是我常用的)

例如:
http://www.cnblogs.com/aierong/admin/EditPosts.aspx?opt=abc
这里?后的opt=1就是查询字符串

当我们在一页面向另一页面传递数据时可以用此方法。

使用如下方法接到数据:
string str=Request.QueryString[“opt“]
这样str就等于“abc“

这里我们传递的是英文字符,要是传递中文字符,我们得进行编码和解吗:
傳送時用Server.UrlEncode方法編碼,接收時用Server.UrlDecode解碼.

例如:
传递:
string url=“http://www.cnblogs.com/aierong/admin/EditPosts.aspx?opt=“+Server.UrlEncode(“我们“);
Response.Redirect(url);
接收:
string str=Server.UrlDecode(Request.QueryString[“opt“]);
这样str就等于“我们“

当然我们还有一省事的方法:

在web.config中修改globalization节为
<globalization
            requestEncoding="gb2312"
            responseEncoding="gb2312"
/>

总结,查询字苻串只可以传递少量数据,因为HTTP WEB服务器不能处理超过255个字符的查询字苻串,查询字苻串最好不要传递安全性高的数据,例如不要传递密码,银行卡号码等数据

九大兵器之二——隐藏的窗体字段

利用隐藏的窗体字段在客户机和服务器间传递数据也是可行的

例如:
<input type="hidden" name="aa" value="abc">

string str=Request.Form["aa"];
str就等于“abc“

总结,最好不要放安全性高的数据,例如不要传递密码,银行卡号码等数据

九大兵器之三——暂存状态

ASP.NET提供一个类System.Web.HttpContext ,用来表示上下文,此对象有一个属性Items

暂存状态就是利用HttpContext.Items属性来存放数据

MSDN中HttpContext.Items属性的解释是这样的:获取可用于在 HTTP 请求过程中在 IHttpModule 和 IHttpHandler 之间组织和共享数据的键值

HttpContext.Items属性中可以存放任何类型数据,无论这个属性中存放什么数据,都会在请求处理结束后自动清除,这就是暂存状态,数据的存放时间非常短.


// 例如:
// 我们有一页面A.ASPX,有一按钮ID:Submit,点按钮把页面转向b.aspx页面

public   void  Submit_Click(Object sender, EventArgs E)
{
    SqlConnection myConnection 
= new SqlConnection("server=(local)NetSDK;database=pubs;Trusted_Connection=yes");
    SqlDataAdapter myCommand 
= new SqlDataAdapter("select * from Authors", myConnection);
    DataSet ds 
= new DataSet();
    myCommand.Fill(ds, 
"Authors");
    
//把数据放入暂存中
    Context.Items["MyData"=ds;
    Server.Transfer(b.aspx);    
}



// b.aspx页面中

public   void  Page_Load(Object sender, EventArgs E)
  
{
    
if (!IsPostBack)
    
{
    
//取得暂存数据
    DataSet ds=(DataSet)Context.Items["MyData"];
    
//其它数据过程
    }

  }

在ibuyspyportal中我们也看到了此功能的使用:

查询字符串包含正被请求的选项卡的 TabIndedx 和 TabId 参数。在处理请求的整个过程中,一直使用此信息筛选要显示给用户的数据。
http://www.ibuyspyportal.com/DesktopDefault.aspx?tabindex=1&tabid=2
要使用查询字符串值,需要首先确保它是一个有效值,如果不是,则要进行一些错误处理。它并不是一大串代码,但是您真的要在每个使用该值的页和组件中复制它吗?当然不!在 Portal 示例中,甚至更多的地方都涉及到它,因为一旦我们知道了 TabId,就可以预先加载其他信息。

Portal 使用查询字符串值作为参数,以构造一个新的 PortalSettings 对象,并将它添加到 Global.asax 的 BeginRequest 事件的 Context.Items 中。由于在每个请求开始处都执行了开始请求,这使得与该选项卡有关的值在应用程序的所有页和组件中都可用。请求完成后,对象将被自动丢弃


void  Application_BeginRequest(Object sender, EventArgs e)
 

     
  
int tabIndex = 0
  
int tabId = 0

  
// 从查询字符串获取 TabIndex 

  
if (Request.Params["tabindex"!= null{        
    tabIndex 
= Int32.Parse(Request.Params["tabindex"]); 
  }
 
         
  
// 从查询字符串获取 TabID 

  
if (Request.Params["tabid"!= null{        
    tabId 
= Int32.Parse(Request.Params["tabid"]); 
  }
 

  Context.Items.Add(
"PortalSettings"new PortalSettings(tabIndex, tabId)); 
}
 

DesktopPortalBanner.ascx 用户控件从 Context 请求 PortalSetting 的对象,以访问 Portal 的名称和安全设置。事实上,此模块是操作中的 Context 的一个典型综合示例。为阐明这一点,我已将代码进行了一些简化,并用粗体标记了 HTTP 或应用程序特定的 Context 被访问过的所有地方。

<% @ Import Namespace="ASPNetPortal"  %>  
<% @ Import Namespace="System.Data.SqlClient"  %>  

< script  language ="C#"  runat ="server" >  

  public 
int     tabIndex; 
  public bool     ShowTabs 
= true
  protected String  LogoffLink 
= ""

  
void Page_Load(Object sender, EventArgs e) 

    
// 从当前上下文获取 PortalSettings 
 PortalSettings portalSettings = 
(PortalSettings) Context.Items[
"PortalSettings"]; 

    
// 动态填充门户站点名称 
    siteName.Text = portalSettings.PortalName; 

    
// 如果用户已登录,自定义欢迎信息 
    if (Request.IsAuthenticated == true
     
      WelcomeMessage.Text 
= "欢迎" + 
Context.User.Identity.Name 
+ "!<" + 
"span class=Accent" + ">|<" + "/span" + ">"

      
// 如果身份验证模式为 Cookie,则提供一个注销链接 
      if (Context.User.Identity.AuthenticationType == "Forms"
        LogoffLink 
= "<" + "span class="Accent">|</span> " + 
"<a href=" + Request.ApplicationPath + 
"/Admin/Logoff.aspx class=SiteLink> 注销" + 
"</a>"
      }
 
    }
 

    
// 动态显示门户选项卡条 
    if (ShowTabs == true

      tabIndex 
= portalSettings.ActiveTab.TabIndex; 

      
// 生成要向用户显示的选项卡列表                  
      ArrayList authorizedTabs = new ArrayList(); 
      
int addedTabs = 0

      
for (int i=0; i < portalSettings.DesktopTabs.Count; i++
       
        TabStripDetails tab 
= 
(TabStripDetails)portalSettings.DesktopTabs[i]; 

        
if (PortalSecurity.IsInRoles(tab.AuthorizedRoles)) 
          authorizedTabs.Add(tab); 
        }
 

        
if (addedTabs == tabIndex) 
          tabs.SelectedIndex 
= addedTabs; 
        }
 

        addedTabs
++
      }
      

      
// 用已授权的选项卡填充页顶部的选项卡 
//
 列表 
      tabs.DataSource = authorizedTabs; 
      tabs.DataBind(); 
    }
 
  }
 

</ script >  
< table  width ="100%"  cellspacing ="0"  class ="HeadBg"  border ="0" >  
  
< tr  valign ="top" >  
    
< td  colspan ="3"  align ="right" >  
      
< asp:label  id ="WelcomeMessage"  runat ="server"   />  
      
< href ="<%= Request.ApplicationPath %>" > Portal 主页 </ a >  
< span  class ="Accent" >  | </ span >  
< href ="<%= Request.ApplicationPath %>/Docs/Docs.htm" >  
        Portal 文档
</ a >  
      
<% =  LogoffLink  %>  
         
    
</ td >  
  
</ tr >  
  
< tr >  
    
< td  width ="10"  rowspan ="2" >  
        
    
</ td >  
    
< td  height ="40" >  
      
< asp:label  id ="siteName"  runat ="server"   />  
    
</ td >  
    
< td  align ="center"  rowspan ="2" >  
     
    
</ td >  
  
</ tr >  
  
< tr >  
    
< td >  
      
< asp:datalist  id ="tabs"  runat ="server" >  
        
< ItemTemplate >  
           
< href ='<%=  Request.ApplicationPath % >  
/DesktopDefault.aspx?tabindex=
<% # Container.ItemIndex  %> &tabid
<% # ((TabStripDetails) Container.DataItem).TabId  %> '> 
<% # ((TabStripDetails) Container.DataItem).TabName  %>  
</ a >   
        
</ ItemTemplate >  
        
< SelectedItemTemplate >  
           
         
< span  class ="SelectedTab" >  
<% # ((TabStripDetails) Container.DataItem).TabName  %>  
</ span >   
        
</ SelectedItemTemplate >  
      
</ asp:datalist >  
    
</ td >  
  
</ tr >  
</ table >  
 

Cookie是一段文本信息,在客户端存储 Cookie 是 ASP.NET 的会话状态将请求与会话关联的方法之一。Cookie 也可以直接用于在请求之间保持数据,但数据随后将存储在客户端并随每个请求一起发送到服务器。浏览器对 Cookie 的大小有限制,因此,只有不超过 4096 字节才能保证被接受。

编写Cookie


// 方式1:
Response.Cookies[ " username " ].value = " mike " ;
Response.Cookies[
" username " ].Expires = DateTime.MaxValue; 

// 方式2:
HttpCookie acookie  =   new  HttpCookie( " last " );
acookie.Value
= " a " ;
acookie..Expires
= DateTime.MaxValue; 
Response.Cookies.Add(acookie);

// 多值Cookie的写法

// 方式1:
Response.Cookies[ " userinfo1 " ][ " name " ].value = " mike " ;
Response.Cookies[
" userinfo1 " ][ " last " ].value = " a " ;
Response.Cookies[
" userinfo1 " ].Expires = DateTime.MaxValue; 

// 方式2:
HttpCookie cookie  =   new  HttpCookie( " userinfo1 " );
cookie.Values[
" name " ] = " mike " ;
cookie.Values[
" last " ] = " a " ;
cookie.Expires
= DateTime.MaxValue; 
Response.Cookies.Add(cookie);
读取Cookie
Internet Explorer 将站点的 Cookie 保存在文件名格式为 <user>@<domain>.txt 的文件中,其中 <user> 是您的帐户名。
注意:在获取Cookie的值之前,应该确保该 Cookie 确实存在。否则,您将得到一个异常

运行此代码时,可看到一个名为“ASP.NET_SessionId”的Cookie,ASP.NET用这个 Cookie 来保存您的会话的唯一标识符。
修改 Cookie
修改的方法与创建方法相同
删除 Cookie 
将其有效期设置为过去的某个日期。当浏览器检查 Cookie 的有效期时,就会删除这个已过期的 Cookie。

If (Request.Cookies[
" userName " ] != null )
{
  
string str = Request.Cookies("userName").Value; 
}


// 多值Cookie的读取
If ( Request.Cookies[ " userInfo1 " ] != null  )
{
  
string name=Request.Cookies["userInfo1"]["name"];
  
string last=Request.Cookies["userInfo1"]["last"]; 
}



// 读取 Cookie 集合
for ( int  i  =   0  ;i < Request.Cookies.Count ;i ++ )
{
    HttpCookie cookies 
= Request.Cookies[i];
    Response.Write(
"name="+cookies.Mame+"<br>");
    
if (cookies.HasKeys )//是否有子键
    {
        System.Collections.Specialized.NameValueCollection NameColl 
                                             = aCookie.Values ;
        
for(int j=0;j<NameColl.Count;j++)
        
{
            Response.Write(
"子键名="+ NameColl.AllKey[j] +"<br>");
            Response.Write(
"子键值="+ NameColl[j] +"<br>");
        }


    }

    
else
    
{
        Response.Write(
"value="+cookies.Value+"<br>");        
    }

}




 


HttpCookie cookie 
=   new  HttpCookie( " userinfo1 " );
cookie.Expires
= DateTime.Now.AddDays( - 30 ); 
Response.Cookies.Add(cookie);
 
九大兵器之五——缓存

ASP.NET 提供一个功能完整的缓存引擎,页面可使用该引擎通过 HTTP 请求存储和检索任意对象.
缓存的生存期与应用程序的生存期相同,也就是说,当应用程序重新启动时,将重新创建缓存。

将数据添加到缓存中

1。通过指定其键和值将项添加到缓存中
Cache["txt"] = "a";

2.通过使用 Insert(重载Insert方法)方法将项添加到缓存中

Cache.Insert("txt", "a");

下列代码显示如何设置相对过期策略。它插入一个项,该项自上次访问后 10 分钟过期。注意 DateTime.MaxValue 的使用,它表示此项没有绝对过期策略。

DateTime absoluteExpiration=DateTime.MaxValue;
TimeSpan slidingExpiration=TimeSpan.FromMinutes(10);
Cache.Insert("txt", "a",null,
absoluteExpiration,slidingExpiration,
System.Web.Caching.CacheItemPriority.High,null);

3.使用 Add 方法将项添加到缓存中
Add 方法与 Insert 方法具有相同的签名,但它返回表示您所添加项的对象

DateTime absoluteExpiration=DateTime.MaxValue;
TimeSpan slidingExpiration=TimeSpan.FromMinutes(10);
Object  Ojb=(string)Cache.Add("txt","a",null,
absoluteExpiration ,slidingExpiration ,
System.Web.Caching.CacheItemPriority.High,null);
string str=(string)Ojb ;
Response.Write(str);

结果显示是"a"

Add 方法使用上没有Insert 方法灵活,使用Add 方法时必须提供7个参数,Insert 方法重载4次,我们可以根据需要选择适当重载方法

从缓存中取得数据

方式1:
string str=(string)Cache.Get("txt");
Response.Write(str);

方式2:
string str1=(string)Cache["txt1"];
Response.Write(str1);

查看Cache中所有数据

System.Text.StringBuilder sb=new System.Text.StringBuilder("",100);
foreach(DictionaryEntry Caches  in Cache)
{
sb.Append("key=").Append(Caches.Key.ToString()).Append("<br>") ;
sb.Append("value=").Append(Caches.Value.ToString()).Append("<br>");
}
Response.Write(sb.ToString());

查看Cache中的项数

int Count=Cache.Count;
Response.Write(Count.ToString());


将数据从缓存中删除

Cache.Remove("txt");

使Cache具有文件依赖项或键依赖项的对象

我们在一页面建立1个按钮,查看CACHE是否存在
在窗体启动时我们创建CACHE,名称="txt2",数值=数据集ds
该CACHE与myfile.xml相关联,当myfile.xml文件变化时,txt2CACHE就被自动删除

private void Page_Load(object sender, System.EventArgs e)
  {
   if( !IsPostBack  )
   {
   string FilePath=MapPath("myfile.xml");
   SqlConnection con=new SqlConnection("Uid=sa;database=pubs;");
   SqlDataAdapter da =new SqlDataAdapter("select * from authors",con);
   DataSet ds=new DataSet();
   da.Fill(ds);
   System.Web.Caching.CacheDependency CacheDependencyXmlFile=new System.Web.Caching.CacheDependency(FilePath);
   Cache.Insert("txt2",ds ,CacheDependencyXmlFile);
   }
  }


为了监视pubs数据库authors表的变化
我们可以在pubs数据库authors表建立触发器
一旦authors表中发生增加,删除,修改数据时,触发器会自动修改文件myfile.xml
一旦myfile.xml文件变化,txt2CACHE就被系统自动删除

CREATE TRIGGER tr_authors
ON authors
FOR INSERT, UPDATE ,delete
AS
declare @cmd nvarchar(4000)
select @cmd='bcp "select convert(nvarchar(30),Getdate(),13)" queryout D:/cache/WebCache/myfile.xml -c -Sglc2403 -Usa -P'
exec master..xp_cmdshell @cmd
GO


private void QueryButton_Click(object sender, System.EventArgs e)
{
if ( Cache["txt2"]!=null)
{
 Response.Write("exists");
}
else
{
 Response.Write("not exists");
}
}

首先我们点按钮,显示Cache["txt2"]存在
现在我们去表authors中任意修改一数据,再点按钮,显示Cache["txt2"]不存在拉


以上我们是把CACHE是和一个文件相关联,我们还可以把CACHE和文件组关联,建立依赖
以下我们把CACHE和2个文件(myfile.xml,myfile1.xml)关联,一旦文件组中其中任意一文件变化,Cache会把"txt2"项的数据从CACHE中删除

string[] FilePath=new String[]{MapPath("myfile.xml"),MapPath("myfile1.xml")};
System.Web.Caching.CacheDependency CacheDependencyXmlFile=new                     System.Web.Caching.CacheDependency(FilePath);
string CacheVaule="a";
Cache.Insert("txt2", CacheVaule ,CacheDependencyXmlFile);


缓存依赖可以是文件,还可以是其他对象的键
下面的代码说明缓存Cache["txt2"]既依赖文件myfile.xml,又依赖缓存中的Cache["txt"],只要这2者任意一样改变,缓存Cache["txt2"]就会清除

Cache["txt"] = "b";
string[] FilePath=new String[]{ MapPath("myfile.xml")};
string[] dependencyKey=new String[]{"txt"};
SqlConnection con=new SqlConnection("Uid=sa;database=pubs;");
SqlDataAdapter da =new SqlDataAdapter("select * from authors",con);
DataSet ds=new DataSet();
da.Fill(ds);
System.Web.Caching.CacheDependency CacheDependencyXmlFile=
          new System.Web.Caching.CacheDependency(FilePath,dependencyKey);
Cache.Insert("txt2",ds ,CacheDependencyXmlFile);

缓存绝对过期

缓存Cache["txt3"] 在1小时后自动过期
DateTime absoluteExpiration =DateTime.Now.AddHours(1);
Cache.Insert("txt3","aa",null,absoluteExpiration,System.Web.Caching.Cache.NoSlidingExpiration);

缓存相对(滑动)过期

注意:如果创建的弹性到期时间小于零或大于一年,则将引发异常
缓存Cache["txt4"] 在最后一次被访问后1小时自动过期
TimeSpan slidingExpiration=TimeSpan.FromHours(1);
Cache.Insert("txt4","4",null,System.Web.Caching.Cache.NoAbsoluteExpiration,slidingExpiration);


缓存项的优先等级

当承载 ASP.NET 应用程序的 Web 服务器缺少内存时,Cache 将有选择地清除项来释放系统内存。当向缓存添加项时,可以为其分配与缓存中存储的其他项相比较的相对优先级。在服务器处理大量请求时,分配了较高优先级值的项被从缓存删除的可能性较小,而分配了较低优先级值的项则更有可能被删除。
由CacheItemPriority 枚举表示,默认为 Normal。

缓存Cache["txt5"]优先等级设为最高等级,在服务器释放系统内存时,该缓存项最不可能被删除。
Cache.Insert("txt5","5",null,
System.Web.Caching.Cache.NoAbsoluteExpiration,
System.Web.Caching.Cache.NoSlidingExpiration,
System.Web.Caching.CacheItemPriority.High,null);

缓存项时通知应用程序的回调方法

ASP.NET 提供 CacheItemRemovedCallback 委托。它定义编写事件处理程序时使用的签名,当从缓存中删除项时,该事件处理程序将进行响应。

static System.Web.Caching.CacheItemRemovedReason reason;
System.Web.Caching.CacheItemRemovedCallback onRemove = null;

public void RemovedCallback(String k, Object v, System.Web.Caching.CacheItemRemovedReason r)
{
 itemRemoved = true;
 reason = r;
}

onRemove = new System.Web.Caching.CacheItemRemovedCallback (this.RemovedCallback);
Cache.Insert("txt",ds,null,
System.Web.Caching.Cache.NoAbsoluteExpiration,
System.Web.Caching.Cache.NoSlidingExpiration,
System.Web.Caching.CacheItemPriority.Default,onRemove);

由于任何原因从Cache中移除时,将调用 RemovedCallback 方法

九大兵器之六——其它物理数据存放媒体

ASP.NET状态存储还可以存放在数据库,XML文件,文本文件,注册表中等等,物理数据存放媒体

数据库
首推ADO.NET

XML
管理XML文档和流主要由System.Xml命名空间中类执行

文件
相关类在System.IO命名空间中
注册表操作由System.Win32命名空间中2个类执行

九大兵器之七——应用程序

可以使用 HttpApplicationState 类在整个应用程序中共享信息,该类通常是通过 HttpContext 对象的 Application 属性进行访问的。该类公开对象的键/值字典,您可以使用该字典来存储 .NET 框架对象和与来自多个客户端的多个 Web 请求相关的标量值。
创建Application
private void Page_Load(object sender, System.EventArgs e)
{
 if( !IsPostBack  )
 {
  SortedList  ApplicationValue =new SortedList();
  ApplicationValue.Add("1","a");
  ApplicationValue.Add("2","b");
  ApplicationValue.Add("3","c");
  ApplicationValue=SortedList.Synchronized(ApplicationValue);
  Context.Application["app"]=ApplicationValue;
  /*
  设置Application的第2种方法
  Add方法,将新的对象添加到 HttpApplicationState 集合中
  Context.Application.Add("app",ApplicationValue);
  */
 }
}
读取Application
private void Button1_Click(object sender, System.EventArgs e)
{
 SortedList  List=(SortedList)Context.Application["app"];
 /*
 取得Application的第2种方法
 Get方法已重载。通过名称或索引获取 HttpApplicationState 对象
 SortedList  List=(SortedList)Context.Application.Get("app");
 或者
 SortedList  List=(SortedList)Context.Application.Get(0);
 */
 System.Text.StringBuilder sb=new System.Text.StringBuilder("");
 if( List!=null )
 {
  sb.Append("app exists").Append("<br>") ;
  for(int i=0;i<List.Count;i++)
  {
   sb.Append((string)List.GetKey(i)).Append("      ");
   sb.Append((string)List.GetByIndex(i)).Append("<br>");
  }
  Response.Write(sb.ToString());
 }
 else
 {
  Response.Write("app not exists");
 }
}
点击按钮后,显示
app exists
Key value
1 a
2 b
3 c
应用程序状态同步

应用程序中的多个线程可以同时访问存储在应用程序状态中的值。因此,当创建需要访问应用程序状态值的对象时,必须始终确保该应用程序状态对象是自由线程的并执行它自己的内部同步,要不就执行手动同步步骤以防止出现争用条件、死锁或访问冲突。

HttpApplicationState 类提供两种方法 Lock 和 Unlock,一次只允许一个线程访问应用程序状态变量。

对 Application 对象调用 Lock 会导致 ASP.NET 阻止运行在其他辅助线程上的代码试图访问应用程序状态中的任何对象。只有当调用 Lock 的线程对 Application 对象调用相应的 Unlock 方法时才解除对这些线程的阻塞。

Application.Lock();
Application["count"]=(int)Application["count"]+1;
Application.UnLock();

如果没有显式调用 Unlock,当请求完成、请求超时或请求执行过程中出现未处理的错误并导致请求失败时,.NET 框架将自动移除锁。这种自动取消锁定会防止应用程序出现死锁。


应用程序需要小心使用复杂对象,例如,集合.集合没有被设计为供多线程同时访问.
利用方法Synchronized()创建集合对象线程安全版本

SortedList  ApplicationValue =new SortedList();
ApplicationValue.Add("1","a");
ApplicationValue.Add("2","b");
ApplicationValue.Add("3","c");
ApplicationValue=SortedList.Synchronized(ApplicationValue);

[END]

九大兵器之四——Cookie
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值