PetShop3.0 Data Access 部分源代码

SQLHelper.cs

//===============================================================================
// This file is based on the Microsoft Data Access Application Block for .NET
// For more information please go to
// http://msdn.microsoft.com/library/en-us/dnbda/html/daab-rm.asp
//===============================================================================

using System;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.Collections;
using PetShop.Utility;

namespace PetShop.SQLServerDAL {

 /// <summary>
 /// The SqlHelper class is intended to encapsulate high performance,
 /// scalable best practices for common uses of SqlClient.
 /// </summary>
 public abstract class SQLHelper {
  
  //Database connection strings
  public static readonly string CONN_STRING_NON_DTC = ConnectionInfo.DecryptDBConnectionString(ConfigurationSettings.AppSettings["SQLConnString1"]);
  public static readonly string CONN_STRING_DTC_INV = ConnectionInfo.DecryptDBConnectionString(ConfigurationSettings.AppSettings["SQLConnString2"]);  
  public static readonly string CONN_STRING_DTC_ORDERS = ConnectionInfo.DecryptDBConnectionString(ConfigurationSettings.AppSettings["SQLConnString3"]);
  
  // Hashtable to store cached parameters
  private static Hashtable parmCache = Hashtable.Synchronized(new Hashtable());

  /// <summary>
  /// Execute a SqlCommand (that returns no resultset) against the database specified in the connection string
  /// using the provided parameters.
  /// </summary>
  /// <remarks>
  /// e.g.: 
  ///  int result = ExecuteNonQuery(connString, CommandType.StoredProcedure, "PublishOrders", new SqlParameter("@prodid", 24));
  /// </remarks>
  /// <param name="connectionString">a valid connection string for a SqlConnection</param>
  /// <param name="commandType">the CommandType (stored procedure, text, etc.)</param>
  /// <param name="commandText">the stored procedure name or T-SQL command</param>
  /// <param name="commandParameters">an array of SqlParamters used to execute the command</param>
  /// <returns>an int representing the number of rows affected by the command</returns>
  public static int ExecuteNonQuery(string connString, CommandType cmdType, string cmdText, params SqlParameter[] cmdParms) {

   SqlCommand cmd = new SqlCommand();

   using (SqlConnection conn = new SqlConnection(connString)) {
    PrepareCommand(cmd, conn, null, cmdType, cmdText, cmdParms);
    int val = cmd.ExecuteNonQuery();
    cmd.Parameters.Clear();
    return val;
   }
  }

  /// <summary>
  /// Execute a SqlCommand (that returns no resultset) against an existing database connection
  /// using the provided parameters.
  /// </summary>
  /// <remarks>
  /// e.g.: 
  ///  int result = ExecuteNonQuery(connString, CommandType.StoredProcedure, "PublishOrders", new SqlParameter("@prodid", 24));
  /// </remarks>
  /// <param name="conn">an existing database connection</param>
  /// <param name="commandType">the CommandType (stored procedure, text, etc.)</param>
  /// <param name="commandText">the stored procedure name or T-SQL command</param>
  /// <param name="commandParameters">an array of SqlParamters used to execute the command</param>
  /// <returns>an int representing the number of rows affected by the command</returns>
  public static int ExecuteNonQuery(SqlConnection conn, CommandType cmdType, string cmdText, params SqlParameter[] cmdParms) {

   SqlCommand cmd = new SqlCommand();

   PrepareCommand(cmd, conn, null, cmdType, cmdText, cmdParms);
   int val = cmd.ExecuteNonQuery();
   cmd.Parameters.Clear();
   return val;
  }

  /// <summary>
  /// Execute a SqlCommand (that returns no resultset) using an existing SQL Transaction
  /// using the provided parameters.
  /// </summary>
  /// <remarks>
  /// e.g.: 
  ///  int result = ExecuteNonQuery(connString, CommandType.StoredProcedure, "PublishOrders", new SqlParameter("@prodid", 24));
  /// </remarks>
  /// <param name="trans">an existing sql transaction</param>
  /// <param name="commandType">the CommandType (stored procedure, text, etc.)</param>
  /// <param name="commandText">the stored procedure name or T-SQL command</param>
  /// <param name="commandParameters">an array of SqlParamters used to execute the command</param>
  /// <returns>an int representing the number of rows affected by the command</returns>
  public static int ExecuteNonQuery(SqlTransaction trans, CommandType cmdType, string cmdText, params SqlParameter[] cmdParms) {
   SqlCommand cmd = new SqlCommand();
   PrepareCommand(cmd, trans.Connection, trans, cmdType, cmdText, cmdParms);
   int val = cmd.ExecuteNonQuery();
   cmd.Parameters.Clear();
   return val;
  }

  /// <summary>
  /// Execute a SqlCommand that returns a resultset against the database specified in the connection string
  /// using the provided parameters.
  /// </summary>
  /// <remarks>
  /// e.g.: 
  ///  SqlDataReader r = ExecuteReader(connString, CommandType.StoredProcedure, "PublishOrders", new SqlParameter("@prodid", 24));
  /// </remarks>
  /// <param name="connectionString">a valid connection string for a SqlConnection</param>
  /// <param name="commandType">the CommandType (stored procedure, text, etc.)</param>
  /// <param name="commandText">the stored procedure name or T-SQL command</param>
  /// <param name="commandParameters">an array of SqlParamters used to execute the command</param>
  /// <returns>A SqlDataReader containing the results</returns>
  public static SqlDataReader ExecuteReader(string connString, CommandType cmdType, string cmdText, params SqlParameter[] cmdParms) {
   SqlCommand cmd = new SqlCommand();
   SqlConnection conn = new SqlConnection(connString);

   // we use a try/catch here because if the method throws an exception we want to
   // close the connection throw code, because no datareader will exist, hence the
   // commandBehaviour.CloseConnection will not work
   try {
    PrepareCommand(cmd, conn, null, cmdType, cmdText, cmdParms);
    SqlDataReader rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection);
    cmd.Parameters.Clear();
    return rdr;
   }catch {
    conn.Close();
    throw;
   }
  }
  
  /// <summary>
  /// Execute a SqlCommand that returns the first column of the first record against the database specified in the connection string
  /// using the provided parameters.
  /// </summary>
  /// <remarks>
  /// e.g.: 
  ///  Object obj = ExecuteScalar(connString, CommandType.StoredProcedure, "PublishOrders", new SqlParameter("@prodid", 24));
  /// </remarks>
  /// <param name="connectionString">a valid connection string for a SqlConnection</param>
  /// <param name="commandType">the CommandType (stored procedure, text, etc.)</param>
  /// <param name="commandText">the stored procedure name or T-SQL command</param>
  /// <param name="commandParameters">an array of SqlParamters used to execute the command</param>
  /// <returns>An object that should be converted to the expected type using Convert.To{Type}</returns>
  public static object ExecuteScalar(string connString, CommandType cmdType, string cmdText, params SqlParameter[] cmdParms) {
   SqlCommand cmd = new SqlCommand();

   using (SqlConnection conn = new SqlConnection(connString)) {
    PrepareCommand(cmd, conn, null, cmdType, cmdText, cmdParms);
    object val = cmd.ExecuteScalar();
    cmd.Parameters.Clear();
    return val;
   }
  }

  /// <summary>
  /// Execute a SqlCommand that returns the first column of the first record against an existing database connection
  /// using the provided parameters.
  /// </summary>
  /// <remarks>
  /// e.g.: 
  ///  Object obj = ExecuteScalar(connString, CommandType.StoredProcedure, "PublishOrders", new SqlParameter("@prodid", 24));
  /// </remarks>
  /// <param name="conn">an existing database connection</param>
  /// <param name="commandType">the CommandType (stored procedure, text, etc.)</param>
  /// <param name="commandText">the stored procedure name or T-SQL command</param>
  /// <param name="commandParameters">an array of SqlParamters used to execute the command</param>
  /// <returns>An object that should be converted to the expected type using Convert.To{Type}</returns>
  public static object ExecuteScalar(SqlConnection conn, CommandType cmdType, string cmdText, params SqlParameter[] cmdParms) {
   
   SqlCommand cmd = new SqlCommand();

   PrepareCommand(cmd, conn, null, cmdType, cmdText, cmdParms);
   object val = cmd.ExecuteScalar();
   cmd.Parameters.Clear();
   return val;
  }

  /// <summary>
  /// add parameter array to the cache
  /// </summary>
  /// <param name="cacheKey">Key to the parameter cache</param>
  /// <param name="cmdParms">an array of SqlParamters to be cached</param>
  public static void CacheParameters(string cacheKey, params SqlParameter[] cmdParms) {
   parmCache[cacheKey] = cmdParms;
  }

  /// <summary>
  /// Retrieve cached parameters
  /// </summary>
  /// <param name="cacheKey">key used to lookup parameters</param>
  /// <returns>Cached SqlParamters array</returns>
  public static SqlParameter[] GetCachedParameters(string cacheKey) {
   SqlParameter[] cachedParms = (SqlParameter[])parmCache[cacheKey];
   
   if (cachedParms == null)
    return null;
   
   SqlParameter[] clonedParms = new SqlParameter[cachedParms.Length];

   for (int i = 0, j = cachedParms.Length; i < j; i++)
    clonedParms[i] = (SqlParameter)((ICloneable)cachedParms[i]).Clone();

   return clonedParms;
  }

  /// <summary>
  /// Prepare a command for execution
  /// </summary>
  /// <param name="cmd">SqlCommand object</param>
  /// <param name="conn">SqlConnection object</param>
  /// <param name="trans">SqlTransaction object</param>
  /// <param name="cmdType">Cmd type e.g. stored procedure or text</param>
  /// <param name="cmdText">Command text, e.g. Select * from Products</param>
  /// <param name="cmdParms">SqlParameters to use in the command</param>
  private static void PrepareCommand(SqlCommand cmd, SqlConnection conn, SqlTransaction trans, CommandType cmdType, string cmdText, SqlParameter[] cmdParms) {

   if (conn.State != ConnectionState.Open)
    conn.Open();

   cmd.Connection = conn;
   cmd.CommandText = cmdText;

   if (trans != null)
    cmd.Transaction = trans;

   cmd.CommandType = cmdType;

   if (cmdParms != null) {
    foreach (SqlParameter parm in cmdParms)
     cmd.Parameters.Add(parm);
   }
  }
 }
}

 

Account.cs

using System;
using System.Data;
using System.Data.SqlClient;
using PetShop.Model;
using PetShop.IDAL;

namespace PetShop.SQLServerDAL {
 /// <summary>
 /// Summary description for AccountDALC.
 /// </summary>
 public class Account : IAccount{

  // Static constants
  private const string SQL_SELECT_ACCOUNT = "SELECT Account.Email, Account.FirstName, Account.LastName, Account.Addr1, Account.Addr2, Account.City, Account.State, Account.Zip, Account.Country, Account.Phone, Profile.LangPref, Profile.FavCategory, Profile.MyListOpt, Profile.BannerOpt FROM Account INNER JOIN Profile ON Account.UserId = Profile.UserId INNER JOIN SignOn ON Account.UserId = SignOn.UserName WHERE SignOn.UserName = @UserId AND SignOn.Password = @Password";
  private const string SQL_SELECT_ADDRESS = "SELECT Account.FirstName, Account.LastName, Account.Addr1, Account.Addr2, Account.City, Account.State, Account.Zip, Account.Country, Account.Phone FROM Account WHERE Account.UserId = @UserId";
  private const string SQL_INSERT_SIGNON = "INSERT INTO SignOn VALUES (@UserId, @Password)";
  private const string SQL_INSERT_ACCOUNT = "INSERT INTO Account VALUES(@UserId, @Email, @FirstName, @LastName, 'OK', @Address1, @Address2, @City, @State, @Zip, @Country, @Phone)";
  private const string SQL_INSERT_PROFILE = "INSERT INTO Profile VALUES(@UserId, @Language, @Category, @ShowFavorites, @ShowBanners)";
  private const string SQL_UPDATE_PROFILE = "UPDATE Profile SET LangPref = @Language, FavCategory = @Category, MyListOpt = @ShowFavorites, BannerOpt = @ShowBanners WHERE UserId = @UserId";
  private const string SQL_UPDATE_ACCOUNT = "UPDATE Account SET Email = @Email, FirstName = @FirstName, LastName = @LastName, Addr1 = @Address1, Addr2 = @Address2, City = @City, State = @State, Zip = @Zip, Country = @Country, Phone = @Phone WHERE UserId = @UserId";
  private const string PARM_USER_ID = "@UserId";
  private const string PARM_PASSWORD = "@Password";
  private const string PARM_EMAIL = "@Email";
  private const string PARM_FIRST_NAME = "@FirstName";
  private const string PARM_LAST_NAME = "@LastName";
  private const string PARM_ADDRESS1 = "@Address1";
  private const string PARM_ADDRESS2 = "@Address2";
  private const string PARM_CITY = "@City";
  private const string PARM_STATE = "@State";
  private const string PARM_ZIP = "@Zip";
  private const string PARM_COUNTRY = "@Country";
  private const string PARM_PHONE = "@Phone";
  private const string PARM_LANGUAGE = "@Language";
  private const string PARM_CATEGORY = "@Category";
  private const string PARM_SHOW_FAVORITES = "@ShowFavorites";
  private const string PARM_SHOW_BANNERS = "@ShowBanners";

  public Account(){
  }

  /// <summary>
  /// Verify the users login credentials against the database
  /// If the user is valid return all information for the user
  /// </summary>
  /// <param name="userId">Username</param>
  /// <param name="password">password</param>
  /// <returns></returns>
  public AccountInfo SignIn(string userId, string password) {

   SqlParameter[] signOnParms = GetSignOnParameters();

   signOnParms[0].Value = userId;
   signOnParms[1].Value = password;

   using (SqlDataReader rdr = SQLHelper.ExecuteReader(SQLHelper.CONN_STRING_NON_DTC, CommandType.Text, SQL_SELECT_ACCOUNT, signOnParms)) {
    if (rdr.Read()) {
     AddressInfo myAddress = new AddressInfo(rdr.GetString(1), rdr.GetString(2), rdr.GetString(3), rdr.GetString(4), rdr.GetString(5), rdr.GetString(6), rdr.GetString(7), rdr.GetString(8), rdr.GetString(9));
     return new AccountInfo(userId, password, rdr.GetString(0), myAddress, rdr.GetString(10), rdr.GetString(11), Convert.ToBoolean(rdr.GetInt32(12)), Convert.ToBoolean(rdr.GetInt32(13)));
    }
    return null;
   }
  }

  /// <summary>
  /// Return the address information for a user
  /// </summary>
  /// <param name="userId"></param>
  /// <returns></returns>
  public AddressInfo GetAddress(string userId) {
   AddressInfo address= null;
   
   SqlParameter[] addressParms = GetAddressParameters();
   
   addressParms[0].Value = userId;
   
   using (SqlDataReader rdr = SQLHelper.ExecuteReader(SQLHelper.CONN_STRING_NON_DTC, CommandType.Text, SQL_SELECT_ADDRESS, addressParms)) {
    if (rdr.Read()) {     
     address = new AddressInfo(rdr.GetString(0), rdr.GetString(1), rdr.GetString(2), rdr.GetString(3), rdr.GetString(4), rdr.GetString(5), rdr.GetString(6), rdr.GetString(7), rdr.GetString(8));      
    }
   }

   return address;
  }

  /// <summary>
  /// Insert a new account info the database
  /// </summary>
  /// <param name="acc">A thin data class containing all the new account information</param>
  public void Insert(AccountInfo acc) {
   SqlParameter[] signOnParms = GetSignOnParameters();
   SqlParameter[] accountParms = GetAccountParameters();
   SqlParameter[] profileParms = GetProfileParameters();

   signOnParms[0].Value = acc.UserId;
   signOnParms[1].Value = acc.Password;

   SetAccountParameters(accountParms, acc);
   SetProfileParameters(profileParms, acc);
       
   using (SqlConnection conn = new SqlConnection(SQLHelper.CONN_STRING_NON_DTC)) {
    conn.Open();
    using (SqlTransaction trans = conn.BeginTransaction()) {
     try {
      SQLHelper.ExecuteNonQuery(trans, CommandType.Text, SQL_INSERT_SIGNON, signOnParms);
      SQLHelper.ExecuteNonQuery(trans, CommandType.Text, SQL_INSERT_ACCOUNT, accountParms);
      SQLHelper.ExecuteNonQuery(trans, CommandType.Text, SQL_INSERT_PROFILE, profileParms);
      trans.Commit();
     
     }catch {
      trans.Rollback();
      throw;
     }
    }
   }
  }

  /// <summary>
  /// Update an account in the database
  /// </summary>
  /// <param name="myAccount">Updated account parameters, you must supply all parameters</param>
  public void Update(AccountInfo myAccount) {
   SqlParameter[] accountParms = GetAccountParameters();
   SqlParameter[] profileParms = GetProfileParameters();
   
   SetAccountParameters(accountParms, myAccount);
   SetProfileParameters(profileParms, myAccount);
       
   using (SqlConnection conn = new SqlConnection(SQLHelper.CONN_STRING_NON_DTC)) {
    conn.Open();
    using (SqlTransaction trans = conn.BeginTransaction()) {
     try {
      SQLHelper.ExecuteNonQuery(trans, CommandType.Text, SQL_UPDATE_ACCOUNT, accountParms);
      SQLHelper.ExecuteNonQuery(trans, CommandType.Text, SQL_UPDATE_PROFILE, profileParms);
      trans.Commit();
     }catch {
      trans.Rollback();
      throw;
     }
    }
   }
  }

  /// <summary>
  /// An internal function to get the database parameters
  /// </summary>
  /// <returns>Parameter array</returns>
  private static SqlParameter[] GetSignOnParameters() {
   SqlParameter[] parms = SQLHelper.GetCachedParameters(SQL_INSERT_SIGNON);

   if (parms == null) {
    parms = new SqlParameter[] {
              new SqlParameter(PARM_USER_ID, SqlDbType.VarChar, 80),
              new SqlParameter(PARM_PASSWORD, SqlDbType.VarChar, 80)};

    SQLHelper.CacheParameters(SQL_INSERT_SIGNON, parms);
   }

   return parms;
  }

  /// <summary>
  /// An internal function to get the database parameters
  /// </summary>
  /// <returns>Parameter array</returns>
  private static SqlParameter[] GetAddressParameters() {
   SqlParameter[] parms = SQLHelper.GetCachedParameters(SQL_SELECT_ADDRESS);

   if (parms == null) {
    parms = new SqlParameter[] {
              new SqlParameter(PARM_USER_ID, SqlDbType.VarChar, 80)};

    SQLHelper.CacheParameters(SQL_SELECT_ADDRESS, parms);
   }

   return parms;
  }

  /// <summary>
  /// An internal function to get the database parameters
  /// </summary>
  /// <returns>Parameter array</returns>
  private static SqlParameter[] GetAccountParameters() {
   SqlParameter[] parms = SQLHelper.GetCachedParameters(SQL_INSERT_ACCOUNT);

   if (parms == null) {
    parms = new SqlParameter[] {
     new SqlParameter(PARM_EMAIL, SqlDbType.VarChar, 80),
     new SqlParameter(PARM_FIRST_NAME, SqlDbType.VarChar, 80),
     new SqlParameter(PARM_LAST_NAME, SqlDbType.VarChar, 80),
     new SqlParameter(PARM_ADDRESS1, SqlDbType.VarChar, 80),
     new SqlParameter(PARM_ADDRESS2, SqlDbType.VarChar, 50),
     new SqlParameter(PARM_CITY, SqlDbType.VarChar, 80),
     new SqlParameter(PARM_STATE, SqlDbType.VarChar, 80),
     new SqlParameter(PARM_ZIP, SqlDbType.VarChar, 50),
     new SqlParameter(PARM_COUNTRY, SqlDbType.VarChar, 50),
     new SqlParameter(PARM_PHONE, SqlDbType.VarChar, 80),
     new SqlParameter(PARM_USER_ID, SqlDbType.VarChar, 80)};

    SQLHelper.CacheParameters(SQL_INSERT_ACCOUNT, parms);
   }

   return parms;
  }

  /// <summary>
  /// An internal function to get the database parameters
  /// </summary>
  /// <returns>Parameter array</returns>
  private static SqlParameter[] GetProfileParameters() {
   SqlParameter[] parms = SQLHelper.GetCachedParameters(SQL_INSERT_PROFILE);

   if (parms == null) {
    parms = new SqlParameter[] {
     new SqlParameter(PARM_LANGUAGE, SqlDbType.VarChar, 80),
     new SqlParameter(PARM_CATEGORY, SqlDbType.VarChar, 50),
     new SqlParameter(PARM_SHOW_FAVORITES, SqlDbType.Int, 4),
     new SqlParameter(PARM_SHOW_BANNERS, SqlDbType.Int, 4),
     new SqlParameter(PARM_USER_ID, SqlDbType.VarChar, 80)};

    SQLHelper.CacheParameters(SQL_INSERT_PROFILE, parms);
   }

   return parms;
  }

  /// <summary>
  /// An internal function to bind values parameters
  /// </summary>
  /// <param name="parms">Database parameters</param>
  /// <param name="acc">Values to bind to parameters</param>
  private void SetAccountParameters(SqlParameter[] parms, AccountInfo acc) {
   parms[0].Value = acc.Email;
   parms[1].Value = acc.Address.FirstName;
   parms[2].Value = acc.Address.LastName;
   parms[3].Value = acc.Address.Address1;
   parms[4].Value = acc.Address.Address2;
   parms[5].Value = acc.Address.City;
   parms[6].Value = acc.Address.State;
   parms[7].Value = acc.Address.Zip;
   parms[8].Value = acc.Address.Country;
   parms[9].Value = acc.Address.Phone;
   parms[10].Value = acc.UserId;
  }

  /// <summary>
  /// An internal function to bind values parameters
  /// </summary>
  /// <param name="parms">Database parameters</param>
  /// <param name="acc">Values to bind to parameters</param>
  private void SetProfileParameters(SqlParameter[] parms, AccountInfo acc) {
   parms[0].Value = acc.Language;
   parms[1].Value = acc.Category;
   parms[2].Value = acc.IsShowFavorites;
   parms[3].Value = acc.IsShowBanners;
   parms[4].Value = acc.UserId;
  }
 }
}

关键是系统架构和代码学习两方面,对初学和提高有很大帮助 petshop5.0比较大,代码已经解压出来 4.0和3.0没有解压出来,自行安装解压(需要SqlServer数据做连接或者在安装到数据库连接时直接拷贝出来) petshop5.0 基于.NET Framework 3.5 ------------ 使用LINQ to SQL改进数据访问层 PetShop.Model.DataContext.MSPetShop4DataContext 继承System.Data.Linq.DataContext PetShop.Model.ProductInfo与PetShop.Model.CategoryInfo实体类分别映射数据库表 PetShop.Model.ProductInfo其中的Category属性存在一对一的关系 PetShop.Model.CategoryInfo中的Products属性存在一对多的关系 使用WCF来提供RSS, web/FeedService.svc目录下 PetShop.SyndicationFeeds 并在UI层上做一些改进,如使用ASP.NET AJAX,ListView控件等。 在PetShop 5.0中引入了异步处理机制。 插入订单的策略可以分为同步和异步,两者的插入策略明显不同,但对于调用者而言,插入订单的接口是完全一样的,所以PetShop 5.0中设计了IBLLStrategy模块。 虽然在IBLLStrategy模块中,仅仅是简单的IOrderStategy,但同时也给出了一个范例和信息,那就是在业务逻辑的处理中,如果存在业务操作的多样化,或者是今后可能的变化,均应利用抽象的原理。或者使用接口,或者使用抽象类,从而脱离对具体业务的依赖。 不过在PetShop中,由于业务逻辑相对简单,这种思想体现得不够明显。 也正因为此,PetShop将核心的业务逻辑都放到了一个模块BLL中,并没有将具体的实现和抽象严格的按照模块分开。所以表示层和业务逻辑层之间的调用关系,其耦合度相对较高: PetShop4.0源代码 .NET Pet Shop4 应用程序的设计说明了构建企业 n 层 .NET 2.0 应用程序的最佳做法,这种应用程序可能需要支持各种数据库平台和部署方案。 .NET Pet Shop 4 项目的目标是: 工作效率:减少了 .NET Pet Shop 3 的代码数量 - 我们减少了近 25% 的代码。 利用 ASP.NET 2.0 的新功能 - 我们利用母版页、成员身份和配置文件,并设计出一个新的、吸引人的用户界面。 企业体系结构:构建一个灵活的最佳做法应用程序 - 我们实现了设计模式,以及表示层、业务层和数据层的分离。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值