基于ArcGIS10.0和Oracle10g的空间数据管理平台四(C#开发)-通用数据管理模块

上一篇文章把整个系统的主界面实现了,接下来就是实现主界面上提供的各个功能模块。首先介绍的是通用数据管理模块,为什么称为通用数据呢?因为这些数据和我们平时使用关系型数据库管理的数据是类似的,这里称为通用数据主要是为了和空间数据做区别。

该模块采用统一的界面来管理所有属性数据表,可以同时做添加、删除和修改,而且对于每一个表都是通用的操作。用户选择修改的表以后就建立一个数据集和这个表关联,再将这个数据集绑定到数据显示控件上,同时将数据集绑定到数据适配器上,当用户对这个表的编辑操作完成以后并点击保存按钮就会将所有的更新操作与数据库同步,这些功能都是调用数据适配器的接口完成。在与数据库同步的时候会先查看是否有与之相关联的表也需要同步更新,如果有就先更新关联的表。为了保证数据库中数据的一致性和完整性,就必须保证所有更新操作都能成功的完成或者都不完成,所以所有更新操作都在一个事务中进行,如果更新过程中遇到异常就回滚到最初状态。整个过程的流程图如下:

第一篇文章介绍这个功能的时候,我贴出了这个模块运行的界面,从界面可以看出主要用到了两个控件,左边是一个树形控件用于按类别显示所有的属性表的名称,通过鼠标就可以选择一个需要查看、修改的表,右边是一个绑定了数据集的表数据显示控件,在这个控件中可以删除、添加和修改表的内容。除了这两个主要控件以外,就是一些用于显示文本提示的label控件和一些按钮控件,按钮主要用于控制用户具体的某种操作,后面会详细介绍每一个按钮功能的实现。

首先定义了一些类的成员变量,每一个成员变量的作用如下代码和注释所示:

private OracleCommandBuilder builder;//用于构建适配器命令 private OracleDataAdapter da; //数据集适配器,用于同步控件与数据库的操作 private DataSet ds;//数据集,可以是一个表也可以是多个表的集合 private string selectedNodeText;//记录树形节点选择的文本 private string tableName;//记录表的名称 protected OracleConnection Connection;//Oracle连接 private bool isChanged = false;//控件绑定的数据是否有改变 private FrmShowLayer fsl;//用于显示有空间对应表的可视化图形(地图)显示 private bool bIdChange = false;//判断表中的ID字段是否有改变,因为ID改变了会涉及到不同的操作 private bool bIdDel = false;//ID字段是否有删除,用于级联删除 private bool bCellValueChange = false;//数据是否有改变 private ArrayList newIdList;//用于保存新增列的ID字段 private ArrayList oldIdList;//用于保存被改变列以前的ID字段 private ArrayList delIdList;//用于保存删除列的ID字段

接着构造函数初始化一些变量:

newIdList = new ArrayList(); oldIdList = new ArrayList(); delIdList = new ArrayList();

在对话框或form的Load函数中初始化一些其他变量:

//Connection = new OracleConnection("Data Source=JCSJK;User Id=dzyj_jcsjk;Password=dzyj_jcsjk"); Connection = new OracleConnection(ConfigurationSettings.AppSettings["ConnectionString"]); Node tn = new Node(); tn.Text = "通用数据库管理"; DataManagerTree.Nodes.Add(tn); tn.Nodes.Add(new Node()); labelX1.Text += FrmMain.username;//显示登录的用户名

上面代码首先初始化Oracle的链接对象,然后为树形控件添加一个根节点,添加具体的表分类目录和具体的表名称节点是在具体展开某一个节点的时候完成,这样启动这个功能界面的时候不会让用户等待太久,这个就是延迟初始化或加载。当然这种方式也有一个不好的地方,就是每次展开一个节点都会去重新初始化,这样会降低一些用户的体验,而且程序执行很多不必要的重复工作。不过这个还是有解决方案的,就是首先判断展开节点下面的子节点是否已经加载,如果已经加载就直接展开就可以了,不用再去重新新添加和初始化节点了。下面来具体解析怎样实现数据的修改操作。

说明:所有的操作都要通过保存按钮功能才是真正的同步到数据库,可以同时做添加、删除和修改操作以后一次性同步到数据库,而且对于所有表都是同样的操作,不过同时只能编辑一个表,如果编辑一个表以后没有点击保存按钮保存到数据库,那么所有的操作将会并取消。

1.删除操作,通过用户点击删除按钮触发,具体代码实现如下:

/// <summary> /// 删除按钮的响应事件实现 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void deleteBtn_Click(object sender, EventArgs e) { //首先判断控件是否允许删除,不允许就让它允许 if (!dataGridViewX1.AllowUserToDeleteRows) { dataGridViewX1.AllowUserToDeleteRows = true; } if (dataGridViewX1.CurrentRow.Index < 0) { MessageBox.Show("请选择需要删除的一行"); return; } //判断是不是这几个表,如果是会涉及到级联删除 if (selectedNodeText == "DZYJ_JCSJK.city_code" || selectedNodeText == "DZYJ_JCSJK.county_code" || selectedNodeText == "DZYJ_JCSJK.town_code" || selectedNodeText == "DZYJ_JCSJK.village_code" || selectedNodeText == "DZYJ_JCSJK.enterprise_code") { if (MessageBox.Show("删除这条记录会删除与之相关的其他表记录,确认删除?", "确认信息", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.Yes) { bIdDel = true; if (delIdList == null) { delIdList = new ArrayList(); } delIdList.Add(dataGridViewX1.CurrentRow.Cells["ID"].Value.ToString()); } else { return; } } //ds.Tables[0].Rows[dataGridViewX1.CurrentRow.Index].Delete(); dataGridViewX1.Rows.Remove(dataGridViewX1.CurrentRow);//移除选择的当前行 isChanged = true;//设置改变为真 saveBtn.Enabled = true;//使能保存按钮,保存按钮实现将改变同步到数据库中去 //dataGridViewX1.Refresh(); }

2.添加操作

/// <summary> /// 添加按钮事件响应函数 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void addBtn_Click(object sender, EventArgs e) { /*if (!dataGridViewX1.AllowUserToAddRows) { dataGridViewX1.AllowUserToAddRows = true; }*/ ((DataTable)dataGridViewX1.DataSource).Rows.Add();//添加一行用于插入数据 if (dataGridViewX1.ReadOnly)//如果控件是只读属性让它可以编辑 { dataGridViewX1.ReadOnly = false; } isChanged = true; saveBtn.Enabled = true; dataGridViewX1.FirstDisplayedScrollingRowIndex = dataGridViewX1.Rows[dataGridViewX1.RowCount-1].Index;//视图定位到添加这一行 }


3.编辑或更新操作(对于数据库的update)

/// <summary> /// 修改按钮事件,使能DataGridView的编辑功能 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void modifyBtn_Click(object sender, EventArgs e) { if (dataGridViewX1.ReadOnly) { dataGridViewX1.ReadOnly = false; } saveBtn.Enabled = true; }


4.保存操作,真正就改变的数据同步到数据库中去,重要功能实现:

/// <summary> /// 更新修改后的数据到数据库中去 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void saveBtn_Click(object sender, EventArgs e) { int result = 0; if (isChanged) { OracleConnection conn = new OracleConnection(ConfigurationSettings.AppSettings["ConnectionString"]); conn.Open(); // 创建一个 OracleTransaction 对象,然后调用 OracleConnection 对象 //的 BeginTransaction() 方法启动事务。 OracleTransaction trans = conn.BeginTransaction(); // 创建一个 OracleCommand 对象,用于存储 SQL 语句。 OracleCommand cmd = conn.CreateCommand(); cmd.Transaction = trans; try { //如果有ID改变或删除就级联更新 if (bIdChange || bIdDel) { UpdateRelationTable(cmd); } result = da.Update(ds, tableName); if (bIdChange || bIdDel) { trans.Commit(); } } catch (Exception ex) { //出现异常就回滚 if (bIdChange || bIdDel) { trans.Rollback(); } MessageBox.Show(ex.Message); } finally { if (bIdChange || bIdDel) { newIdList.Clear(); oldIdList.Clear(); delIdList.Clear(); } } } isChanged = false; bIdDel = false; bIdChange = false; dataGridViewX1.ReadOnly = true; dataGridViewX1.AllowUserToAddRows = false; dataGridViewX1.AllowUserToDeleteRows = false; saveBtn.Enabled = false; //如果有数据行被更新到数据库,会把具体的操作记录到日志表中 if (result > 0) { //0表示普通表,1空间表 Node tn = DataManagerTree.FindNodeByText(selectedNodeText); if (tn.Parent.Text != "属性表") { LogHelp.writeUpdateDataLog(tableName, "1", "修改"); } else { LogHelp.writeUpdateDataLog(tableName, "0", "修改"); } LogHelp.writeLog(FrmMain.username, "更新通用数据", "通用数据表" + tableName + "更新成功"); MessageBox.Show("更新数据库成功"); } else { LogHelp.writeLog(FrmMain.username, "更新通用数据", "通用数据表" + tableName + "更新失败"); MessageBox.Show("数据库没有更新"); } }

这个函数功能相对复杂,因为涉及到级联更新操作,不是使用的触发器功能,因为不能使用触发器,他们的关联情况是根据ID字段部分内容进行关联的,可能是前几位不等。所以实现这个功能就相对复杂,我采用的方式是对这些关联关系我通过一张数据表来维护,通过这张数据表就很轻松查出有关联关系的表的所有内容,根据这些内容就可以做到级联更新了,不过级联更新都是采用的事务操作来保证数据的一致性,级联更新在单独一个函数中实现,函数代码如下:

/// <summary> /// 更新于相关的表 /// </summary> /// <param name="cmd">带事务的命令</param> private void UpdateRelationTable(OracleCommand cmd) { //处理有ID改变的情况 SqlHelper sh = new SqlHelper(); string sql = string.Empty; if (bIdChange) { //更新相关的表 for (int i = 0; i < oldIdList.Count; ++i ) { //1.查询数据字典找到所有需要更新的表 //string strTable = tableName.Substring(tableName.IndexOf('.')+1); sql = "select second,bits from jcsjk_relation where first='" + tableName + "'"; OracleDataReader odr = sh.ReturnDataReader(sql); while (odr.Read()) { //得到ID关联的位数 int bits = int.Parse(odr[1].ToString()); //得到完整的表名称,包括表的拥有者 string strTemp = odr[0].ToString(); //得到被改变的ID的前bits位 string strOldId = oldIdList[i].ToString().Substring(0, bits); //查询需要更新的相关ID string strAll = odr[0].ToString(); sql = "select id from " + strAll + " where id like '" + strOldId + "%'"; OracleDataReader odr1 = sh.ReturnDataReader(sql); while (odr1.Read()) { //根据前bits位构建新的ID string strNewId = newIdList[i].ToString().Substring(0, bits); strNewId += odr1[0].ToString().Substring(bits); //根据查询出来的旧ID更新ID sql = "update " + strTemp + " set id='" + strNewId + "' where id ='" + odr1[0].ToString() + "'"; cmd.CommandText = sql; cmd.ExecuteNonQuery(); } } } } //处理有ID删除的情况 if (bIdDel) { foreach (string str in delIdList) { //查询和删除ID相关的表并执行事务删除 //string strTable = tableName.Substring(tableName.IndexOf('.') + 1); sql = "select second,bits from jcsjk_relation where first='" + tableName + "'"; OracleDataReader odr = sh.ReturnDataReader(sql); while (odr.Read()) { string strTemp = odr[0].ToString(); string strOldId = str.Substring(0, int.Parse(odr[1].ToString())); sql = "delete from " + strTemp + " where id like '" + strOldId + "%'"; cmd.CommandText = sql; cmd.ExecuteNonQuery(); } } } }

保存按钮事件的功能除了同步改变的数据到数据库以外,还会对具体的操作记录日志并写入数据库日志表中,这个日志主要用于多台数据库服务器的同步操作。

5.绑定数据的控件数据单元的值发生变化产生时的响应函数代码如下:

/// <summary> /// 如果ID这一列的值有改变就保存改变后的值,原来的值在开始编辑事件中保存,如果值没有改变,将在编辑结束 /// 删除掉保存的原理的ID值,以保证原来的ID值与改变后的ID值一一对应,方便级联更新和删除 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void dataGridViewX1_CellValueChanged(object sender, DataGridViewCellEventArgs e) { if (dataGridViewX1.Columns[e.ColumnIndex].HeaderText == "ID") { if (selectedNodeText == "DZYJ_JCSJK.city_code" || selectedNodeText == "DZYJ_JCSJK.county_code" || selectedNodeText == "DZYJ_JCSJK.town_code" || selectedNodeText == "DZYJ_JCSJK.village_code" || selectedNodeText == "DZYJ_JCSJK.enterprise_code") { bIdChange = true; //保存新ID newIdList.Add(dataGridViewX1.Rows[e.RowIndex].Cells[e.ColumnIndex].Value.ToString()); bCellValueChange = true; } } isChanged = true; saveBtn.Enabled = true; }


6.当用户在输入数据非法是执行如下函数;

/// <summary> /// 提示用户录入数据非法 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void dataGridViewX1_DataError(object sender, DataGridViewDataErrorEventArgs e) { labelX6.Text = "数据录入格式不正确"; }


7.导出数据到excel或word:

/// <summary> /// 导出数据到excel和word /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void exportBtn_Click(object sender, EventArgs e) { //选择导出的类型,excel或word FrmSelectExpType fset = new FrmSelectExpType(); fset.ShowDialog(); if (fset.isGon) { //根据选择的类型执行相应的导出功能 if (fset.type == 0) { //调用通用工具类的静态函数导出为word CommonTools.ExportDataGridViewToWord(dataGridViewX1); } else { 调用通用工具类的静态函数导出为word CommonTools.DataToExcel(dataGridViewX1); MessageBox.Show("导出数据完成!"); } } }

8.滚动显示数据表的所有数据:

/// <summary> /// 显示所有的数据,可以滚动 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void showAllBtn_Click(object sender, EventArgs e) { dataGridViewX1.ScrollBars = ScrollBars.Both; try { dataGridViewX1.FirstDisplayedScrollingRowIndex = 0; } catch (ArgumentOutOfRangeException) { } }


9.分页显示函数功能:

/// <summary> /// 分页就是通过设置第一个显示的行来实现 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void comboBoxEx1_SelectedValueChanged(object sender, EventArgs e) { try { dataGridViewX1.FirstDisplayedScrollingRowIndex = comboBoxEx1.SelectedIndex * 10; } catch (ArgumentOutOfRangeException) { } }


10.导入excel的数据到控件中(DataGridView控件):

/// <summary> /// 导入一个选择的excel文件到DataGridView中 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void importBtn_Click(object sender, EventArgs e) { //打开一个文件选择框 OpenFileDialog ofd = new OpenFileDialog(); ofd.Title = "Excel文件"; ofd.FileName = ""; //为了获取特定的系统文件夹,可以使用System.Environment类的静态方法GetFolderPath()。 //该方法接受一个Environment.SpecialFolder枚举,其中可以定义要返回路径的哪个系统目录 ofd.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); ofd.Filter = "Excel文件(*.xls)|*.xls"; //文件有效性验证ValidateNames,验证用户输入是否是一个有效的Windows文件名 ofd.ValidateNames = true; ofd.CheckFileExists = true; //验证路径有效性 ofd.CheckPathExists = true; //验证文件有效性 if (ofd.ShowDialog() == DialogResult.Cancel) { return; } if (ofd.FileName == "") { MessageBox.Show("没有选择Excel文件!无法进行数据导入"); return; } //调用导入数据方法 CommonTools.EcxelToDataGridView(ofd.FileName, dataGridViewX1); MessageBox.Show("导入数据完成!"); }


11.浏览地图功能实现:

/// <summary> /// 浏览地图 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void mapBrowseBtn_Click(object sender, EventArgs e) { Node tn = DataManagerTree.FindNodeByText(selectedNodeText); string tabName = "地图浏览"; if (!((FrmMain)this.MdiParent).IsOpenTab(tabName)) { fsl = new FrmShowLayer(); fsl.MdiParent = MdiParent; fsl.WindowState = FormWindowState.Maximized; this.WindowState = FormWindowState.Normal; fsl.Show(); } /* if (fsl == null) { fsl = new FrmShowLayer(); } fsl.WindowState = FormWindowState.Maximized; fsl.Show();*/ if (tn.Parent.Text != "属性表") { fsl.AddLayerToMapCtl(tn.Text.ToUpper(), true); } }


12.树形节点选中时功能的处理,不同节点有不同的处理方式(同一层是同样的功能,只是处理的数据不同而已):

/// <summary> /// 选中一个树节点的时候对应不同层次做不同处理,可以显示选中的数据集, /// 选中一个表就在DataGridView中显示表的数据 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void DataManagerTree_AfterNodeSelect(object sender, AdvTreeNodeEventArgs e) { //DataGridView中没有数据时禁止导入导出和浏览地图功能 mapBrowseBtn.Enabled = false; importBtn.Enabled = false; exportBtn.Enabled = false; addBtn.Enabled = false; modifyBtn.Enabled = false; deleteBtn.Enabled = false; showAllBtn.Enabled = false; Node tn = new Node(); tn = e.Node; selectedNodeText = tn.Text; switch (tn.Level) { case 1: labelX3.Text = tn.Text; break; case 2: //使能功能按钮 addBtn.Enabled = true; modifyBtn.Enabled = true; deleteBtn.Enabled = true; showAllBtn.Enabled = true; isChanged = false; bIdDel = false; bIdChange = false; dataGridViewX1.ReadOnly = true; dataGridViewX1.AllowUserToAddRows = false; dataGridViewX1.AllowUserToDeleteRows = false; delIdList.Clear(); newIdList.Clear(); oldIdList.Clear(); labelX3.Text = e.Node.Parent.Text; labelX4.Text = ""; labelX4.Text = "当前数据表: " + tn.Text; SqlHelper sh = new SqlHelper(); string[] first = tn.Text.Split('.'); string sql = "select column_name,data_type from all_tab_columns where table_name='" + first[1].ToUpper() + "' and owner='" + first[0].ToUpper() + "'"; if (sh.GetRecordCount(sql) <= 0) { MessageBox.Show("数据表不存在!"); return; } tableName = tn.Text; OracleDataReader odr = sh.ReturnDataReader(sql); //动态构建显示的sql语句,填充的字段 sql = "select "; while (odr.Read()) { //当DataGridView中有数据时就可以导入导出了 importBtn.Enabled = true; exportBtn.Enabled = true; if (odr[0].ToString() == "SHAPE") { //当DataGridView中有空间数据时就可以浏览地图了 mapBrowseBtn.Enabled = true; //DataGridView不能显示Shape字段 continue; } //如果数据类型是BLOB就不加载 if (odr[1].ToString() == "BLOB") { continue; } sql += odr[0].ToString() + ","; } //移除最后一个逗号 sql = sql.Remove(sql.LastIndexOf(',')); sql += " from " + tableName; if (Connection.State != ConnectionState.Open) { Connection.Open(); } //构建数据适配器为了修改数据,绑定的数据表必须有主键才能修改 da = new OracleDataAdapter(sql, Connection); builder = new OracleCommandBuilder(da); ds = new DataSet(); try { da.Fill(ds, tableName); dataGridViewX1.DataSource = null; dataGridViewX1.DataSource = ds.Tables[0]; } catch (System.Exception ex) { MessageBox.Show(ex.Message); } //以下是实现分页显示 int intMod, dgr; //先让垂直滚动条消失 dataGridViewX1.ScrollBars = ScrollBars.Horizontal; //取出DGV的行数,为什么要减一是因为它总是多出一行给你编辑的所以那行也占用一行的空间 dgr = dataGridViewX1.RowCount - 1; //进行取模 if (dgr % 10 == 0) { intMod = 0; } else { intMod = 1; } //主要时这个for循环将表一共分为几页添加到comboBox comboBoxEx1.Items.Clear(); for (int i = 1; i <= dgr / 10 + intMod; i++) { comboBoxEx1.Items.Add("第" + i + "页"); } //默认选中第一个 if (comboBoxEx1.Items.Count > 0) { comboBoxEx1.SelectedIndex = 0; } break; default: break; } }


13.树形节点被展开时执行的相应功能实现如下:

/// <summary> /// 节点展开以后显示下层节点 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void DataManagerTree_AfterExpand(object sender, AdvTreeNodeEventArgs e) { Node tn = e.Node; string sql; SqlHelper sh = new SqlHelper(); OracleDataReader odr; switch (tn.Level) { case 0: tn.Nodes.Clear(); sql = "select name from jcsjk_element where category='矢量数据'"; odr = sh.ReturnDataReader(sql); while (odr.Read()) { Node t = new Node(); t.Text = odr[0].ToString(); tn.Nodes.Add(t); t.Nodes.Add(new Node()); } break; case 1: tn.Nodes.Clear(); sql = "select table_name from jcsjk_layer l,jcsjk_element e where l.pid=e.id and e.name = '" + tn.Text + "' and e.category='矢量数据'"; odr = sh.ReturnDataReader(sql); while (odr.Read()) { Node t = new Node(); t.Text = odr[0].ToString(); tn.Nodes.Add(t); } break; default: break; } }


14.开始编辑控件数据的时候执行的函数功能实现如下:

/// <summary> /// 开始编辑事件,如果编辑的是ID列就保存原来的ID /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void dataGridViewX1_CellBeginEdit(object sender, DataGridViewCellCancelEventArgs e) { if (dataGridViewX1.Columns[e.ColumnIndex].HeaderText == "ID") { if (selectedNodeText == "DZYJ_JCSJK.city_code" || selectedNodeText == "DZYJ_JCSJK.county_code" || selectedNodeText == "DZYJ_JCSJK.town_code" || selectedNodeText == "DZYJ_JCSJK.village_code" || selectedNodeText == "DZYJ_JCSJK.enterprise_code") { if (oldIdList == null) { oldIdList = new ArrayList(); } //保存旧ID if (dataGridViewX1.Rows[e.RowIndex].Cells[e.ColumnIndex].Value.ToString() != "") { oldIdList.Add(dataGridViewX1.Rows[e.RowIndex].Cells[e.ColumnIndex].Value.ToString()); } } } }


15.结束编辑控件数据的时候执行的函数功能实现如下:

/// <summary> /// 编辑结束事件,如果ID值没有改变就删除原来保存的 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void dataGridViewX1_CellEndEdit(object sender, DataGridViewCellEventArgs e) { if (bCellValueChange) { bCellValueChange = false; } else { if (dataGridViewX1.Columns[e.ColumnIndex].HeaderText == "ID") { if (selectedNodeText == "DZYJ_JCSJK.city_code" || selectedNodeText == "DZYJ_JCSJK.county_code" || selectedNodeText == "DZYJ_JCSJK.town_code" || selectedNodeText == "DZYJ_JCSJK.village_code" || selectedNodeText == "DZYJ_JCSJK.enterprise_code") { if (dataGridViewX1.Rows[e.RowIndex].Cells[e.ColumnIndex] != null) { oldIdList.Remove(dataGridViewX1.Rows[e.RowIndex].Cells[e.ColumnIndex].Value.ToString()); } } } } }

到此整个同样数据管理模块的功能已经实现,这里需要强调一点的是,当在判断对于的属性数据是否对应有相应的空间数据时就是判断对于的表结构是否有“Shape”字段,空间数据里面还涉及到很多概念以后在介绍管理空间数据时会详细介绍。还有一点就是上面实现图层的可视化显示(地图)后面会详细介绍,至于word和excel的导入导出功能是在一个通过用的工具类中实现,以方便整个程序中都可以使用,日志的写入也是专门的日志帮助类实现。




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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值