四则运算——软工结对项目

队友博客地址链接:Xie_mixue的博客

Github源码地址:源代码

目录
一、问题描述
二、PSP表格预估时间
三、Console阶段
四、GUI阶段
五、单元测试
六、重要优化
七、性能分析
八、最终成果展示
九、PSP表格实际耗时
十、总结

一、问题描述

1.一次生成一千个小学四则运算题目到一个文件里,保证合法不重复
注意:
a.运算符至多10个,其中括号数不在此限制内。
b.可以进行真分数运算。
c.可以设置对乘方的符号选择( ** 或 ^ )。
d.合法的注意事项:没有除0的操作括号匹配真分数运算可能会出现假分数结果,此时将结果表示为 整数+真分数。

2.一个图形界面,用户可以输入答案,系统判断对错,并设立一个20秒的倒计时,并创建用户答题历史记录。

二、PSP表格预估时间

PSP2.1Personal Software Process Stages预估耗时(min)实际耗时(min)
Planning计划20
Estimate估计这个任务需要多少时间20
Development开发2880
Analysis需求分析(包括学习新技术)1440
Design Spec生成设计文档0
Design Review设计复审(和同事审核设计文档)0
Coding Standard代码规范(为目前的开发制定合适的规范)0
Design具体设计60
Coding具体编码5760
Code Review代码复审60
Test测试(自我测试,修改代码,提交修改)1000
Reporting报告0
Test Report测试报告60
Size Measurement计算工作量60
Postmortem & Process Improvement Plan事后总结,并提出过程改进计划60
合计11360

三、Console阶段

本阶段的完整源代码地址:Console_Version
经过商量之后,决定暂时将任务划分为
1.创建题目
2.计算题目
  我的任务为第二个,基本思路为,读取题目(一条字符串),分类,计算。
计算的难点在于运算优先级,基本规则可以概括为(从栈的角度看):
(1)当前运算符为加、减号时,之前的加、减、乘、除、乘方都要计算掉(乘、除、乘方优先级高于加减号);
(2)当前运算符为乘、除号时,之前的乘、除、乘方要计算掉(同一优先级自左向右计算);
(3)乘方的优先级最高,且乘方自右向左计算
(4)左括号直接入栈,continue;
(5)遇到右括号时,一直计算直到弹出距离栈顶最近的一个左括号,右括号不入栈;
(6)要将分数的结果化简为最简式,并要考虑若分子为0导致最终结果为0、若结果为-1/1要表示为-1,若分子大于分母则表示为如2+2/3的格式;
(7)考虑连续7个以上9相乘的情况,设为long long int。
  此外,因为设计题目的时候就考虑了题目的合法性,因此在计算除法时不用考虑除数为0的情况。
第一版的命令行界面示例:
在这里插入图片描述
在这里插入图片描述

经过计算器验证,答案正确。

以下为设计的参数和函数名称列表

stack<long long int> Data; //数字
stack<char> Operator;  
stack<long long int> Numerator;//分子
stack<long long int> Denominator;//分母
string result;
int PowModel = 0;//默认乘方为 ^
string userAnswer;

#pragma region MainFunc
bool CheckQuestion(string Question);
void Clear();//清空栈
void Calculate(string Question);
void UserGUI();
string Transform(int data);//将数值转换为字符串
#pragma endregion

#pragma region Integer
void ReadInt(string Question);
void Integer();//+-*/^
void CalInt();//确保Operator栈为空
string ToPower(string Question);//将 ** 转换为 ^
#pragma endregion

#pragma region Fraction
void ReadFraction(string Question);
int gcd(int x, int y);//最大因约数
int lcm(int x, int y);//最小公倍数 = 两数相乘 / 最大因约数
void Fraction();//+-*/
void CalFraction();
void Simple();//分数最简化
#pragma endregion

1.整数计算核心函数 ReadInt()

void ReadInt(string Question) {
	for (int i = 0; Question[i] != '='; i++) {
		//------------空格 + 左括号-----------
		if (Question[i] == ' ') {
			continue;
		}
		if (Question[i] == '(') {
			Operator.push('(');
			continue;
		}
		//---------------数字部分---------------
		int number = 0;
		if (Question[i] >= '0' && Question[i] <= '9') {
			while (Question[i] >= '0' && Question[i] <= '9') {
				number = number * 10 + Question[i] - '0';
				i++;
			}
			i--;
			Data.push(number);
			continue;
		}
		//----------------运算符 + 计算----------------
		if ((Question[i] < '0' || Question[i] > '9') && Question[i] != ' ') {
			if (Question[i] == ')') {//当前运算符为右括号
				while (Operator.top() != '(') {
					Integer();
				}
				Operator.pop();
			}
			else if (Question[i] == '^') {//当前运算符为乘方,直接放入栈中即可
				Operator.push(Question[i]);
			}
			else if(Question[i] != '^'){//如果当前运算符不为乘方
				if (Question[i] == '(') {
					Operator.push(Question[i]);
					continue;
				}
				while (!Operator.empty() && Operator.top() == '^') {//先计算之前运算符里的乘方,从右到左
					Integer();
				}
				if (!Operator.empty() &&( Operator.top() == '*' || Operator.top() == '/' ) ) {//当栈顶元素不为乘方后,乘除号优先
					while (!Operator.empty() && (Operator.top() != '+' && Operator.top() != '-' && Operator.top()!='(')) {
						Integer();
					}
				}
				if (!Operator.empty() && (Question[i] == '+' || Question[i] == '-')) {//同一优先级自左向右算
					while (!Operator.empty() && Operator.top() != '(') {
						Integer();
					}
				}
				Operator.push(Question[i]);
			}			
		}
	}
}

2.分数计算核心函数ReadFraction()

void ReadFraction(string Question) {
	for (int i = 0; Question[i] != '='; i++) {
		//---------------------括号处理-------------
		if (Question[i] == '(') {   //左括号直接入栈
			Operator.push('(');
			i++;//跳过之后的空格
			continue;
		}
		if (Question[i] == ')') {//右括号不入栈,并会计算直到弹出距离栈顶最近的一个左括号
			while (!Operator.empty() && Operator.top() != '(') {
				Fraction();
			}
			Operator.pop();//弹出左括号
			i++;//跳过之后空格
			if (Question[i] == '=') {
				return;
			}
			continue;
		}
		//--------------------分子分母计算---------------------
		int number = 0;
		int flag = 0;
		while (Question[i] >= '0' && Question[i] <= '9') {
			flag = 1;
			number = number * 10 + Question[i] - '0';
			i++;
		}
		if (flag != 0 && Question[i] == '/') {//后一位为 / 则为分子
			Numerator.push(number);
			continue;
		}
		else if (flag != 0 && Question[i] == ' ') {
			Denominator.push(number);
			continue;
		}
		//--------------------运算符入栈 + 计算-------------------------
		if (Question[i] == ' ') {
			char before = Question[i - 1];
			if (Operator.empty() || Operator.top() == '(') {//如果栈为空,或者栈顶元素为左括号,那么此运算符入栈,然后跳过
				Operator.push(before);
				continue;
			}
			else {
				if (!Operator.empty() && (before == '+' || before == '-')) {
					while (!Operator.empty() && Operator.top() != '(') {//加减号之前的乘除加减都要先算
						Fraction();
					}
					
				}
				else if (!Operator.empty() &&(before == '*' || before == '/')) {//乘除号之前的乘除号要先算
					if (!Operator.empty() && Operator.top() != '+' && Operator.top() != '-') {
						Fraction();
					}
				}
				Operator.push(before);
			}
		}
	}
}

其他函数(如simple()、Clear()等)会占用大量版面不予展示。

四、GUI阶段

本阶段完整代码地址:GUI_Version
  经过讨论后我们选择C#语言中的winform来设计。
  在C++版本的代码中,整数计算中除法为计算机的默认整数除法,即3/2=1,1/2=0,这样的结果误差结果较大,考虑设计一个函数,当整数题目中存在除法时将题目转换为分母为1的分数进行计算,即可避免除法误差。
  但进一步讨论后,发现在创建题目的过程中,因为需要进行题目查重所以需要对题目进行计算,这时候已经可以直接计算出题目结果,并且可以将所有数字都按分数来计算(整数为分母为1的分数),于是设立string[] puzzle_str 来存储题目,string[] answer_str来存储每道题的运算结果。
  在向RecordGUI窗口传递用户答题记录时,在PuzzleGUI窗口设立一个string[] History来保存用户的答题记录。

五、单元测试

大致分为分为题目查重测试和运算符重载测试两个部分。
本部分完整代码地址:单元测试代码
截取代码块注释如下:

//-------------------获取最大公因数函数测试---------
public void FracionGetGCDTest1()//正常情况
public void FracionGetGCDTest2()//最大公因数为1
public void FracionGetGCDTest3()//最大公因数为其中较小的数
//--------------------题目查重测试------------------
//加法交换+括号测试
public void CheckTest1() //测试1+2+3与3+(2+1)是否重复,预期结果为重复 -1
//加法顺序交换测试
public void CheckTest2()//测试1+2+3与3+2+1是否重复,预期结果为不重复 1
//乘法顺序交换测试
public void CheckTest3()//测试123与321是否重复,预期结果为不重复 1
//除法顺序交换测试
public void CheckTest4()//测试1/2/3与3/2/1是否重复,预期结果为不重复 1
//乘法交换+括号测试
public void CheckTest5()//测试(1+2)3与3(2+1)是否重复,预期结果为重复 -1
//^乘方测试
public void CheckTest6()//测试1+231与231 + 1是否重复,预期结果为重复 -1
//括号嵌套测试
public void CheckTest7()//测试 (1+(1+2)(3-2))+1 与 1+((3-2)(1+2))+1) 是否重复,预期结果为不重复 1
//括号嵌套测试+乘法顺序变换
public void CheckTest8()//测试 (1+(1+2)(3-2))+1 与 1+((1+2)(3-2))+1) 是否重复,预期结果为重复 -1
//乘方测试
public void CheckTest9()//测试1+2
31与23**1 + 1是否重复,预期结果为重复 -1
//分数的加法+括号测试
public void CheckTest10()//测试 1/2 + (1/3 + 1/4) 与 1/4 + 1/3 + 1/2 是否重复,预期结果为重复 -1
//分数的加法+乘法测试
public void CheckTest11() //测试 1/2 * (1/3 + 1/4) 与 (1/4 + 1/3) * 1/2 是否重复,预期结果为重复 -1
//--------------运算符重载测试-------------------------
public void CheckTest12()//分数类 ^ 运算符重载
public void CheckTest13()//分数类 * 运算符重载
public void CheckTest14()//分数类 / 运算符重载
public void CheckTest15()//分数类 + 运算符重载
public void CheckTest16()//分数类 - 运算符重载

运行测试后截图:
在这里插入图片描述
典型测试用例的代码摘取:

     //--------------------题目查重测试------------------
        //加法交换+括号测试
        [TestMethod()]//测试1+2+3与3+(2+1)是否重复,预期结果为重复 -1
        public void CheckTest1()
        {
            N_Puzzle.labels[0] = 0;
            N_Puzzle.labels[1] = 0;
            int[] puzzle1 = new int[] { 1, 100, 2, 100, 3 };
            int[] puzzle2 = new int[] { 3, 100, 106, 2, 100, 1, 107 };
            int puzzle_len1 = 5;
            int puzzle_len2 = 7;
            int puzzle_num1 = 0;
            int puzzle_num2 = 1;
            int num_type1 = 0;
            int num_type2 = 0;
            N_Puzzle.Check(puzzle1, puzzle_len1, puzzle_num1, num_type1);
            Assert.AreEqual(N_Puzzle.Check(puzzle2, puzzle_len2, puzzle_num2, num_type2), -1);
        }
        
        //乘法顺序交换测试
        [TestMethod()]//测试1*2*3与3*2*1是否重复,预期结果为不重复 1
        public void CheckTest3()
        {
            N_Puzzle.labels[0] = 0;
            N_Puzzle.labels[1] = 0;
            int[] puzzle1 = new int[] { 1, 102, 2, 102, 3 };
            int[] puzzle2 = new int[] { 3, 102, 2, 102, 1 };
            int puzzle_len1 = 5;
            int puzzle_len2 = 5;
            int puzzle_num1 = 0;
            int puzzle_num2 = 1;
            int num_type1 = 0;
            int num_type2 = 0;
            N_Puzzle.Check(puzzle1, puzzle_len1, puzzle_num1, num_type1);
            Assert.AreEqual(N_Puzzle.Check(puzzle2, puzzle_len2, puzzle_num2, num_type2), 1);
        }
        
        //括号嵌套测试+乘法顺序变换
        [TestMethod()]//测试 (1+(1+2)*(3-2))+1 与 1+((1+2)*(3-2))+1) 是否重复,预期结果为重复 -1
        public void CheckTest8()
        {
            N_Puzzle.labels[0] = 0;
            N_Puzzle.labels[1] = 0;
            int[] puzzle1 = new int[] { 106, 1, 100, 106, 1, 100, 2, 107, 102, 106, 3, 101, 2, 107, 107, 100, 1 };
            int[] puzzle2 = new int[] { 1, 100, 106, 106, 1, 100, 2, 107, 102, 106, 3, 101, 2, 107, 107, 100, 1 };
            int puzzle_len1 = 17;
            int puzzle_len2 = 17;
            int puzzle_num1 = 0;
            int puzzle_num2 = 1;
            int num_type1 = 0;
            int num_type2 = 0;
            N_Puzzle.Check(puzzle1, puzzle_len1, puzzle_num1, num_type1);
            Assert.AreEqual(N_Puzzle.Check(puzzle2, puzzle_len2, puzzle_num2, num_type2), -1);
        }
        
       
        //分数的加法+乘法测试
        [TestMethod()]//测试 1/2 * (1/3 + 1/4) 与 (1/4 + 1/3) * 1/2 是否重复,预期结果为重复 -1
        public void CheckTest11()
        {
            N_Puzzle.labels[0] = 1;
            N_Puzzle.labels[1] = 1;
            int[] puzzle1 = new int[] { 1, 1030, 2, 102, 106, 1, 1030, 3, 100, 1, 1030, 4, 107 };
            int[] puzzle2 = new int[] { 106, 1, 1030, 4, 100, 1, 1030, 3, 107, 102, 1, 1030, 2 };
            int puzzle_len1 = 13;
            int puzzle_len2 = 13;
            int puzzle_num1 = 0;
            int puzzle_num2 = 1;
            int num_type1 = 1;
            int num_type2 = 1;
            N_Puzzle.Check(puzzle1, puzzle_len1, puzzle_num1, num_type1);
            Assert.AreEqual(N_Puzzle.Check(puzzle2, puzzle_len2, puzzle_num2, num_type2), -1);
        }
		[TestMethod()] //分数类 ^ 运算符重载
        public void CheckTest12()
        {
            bool z = false;
            fraction A = new fraction (2,3);
            fraction B = new fraction (0, 1);
            fraction C = A ^ B;
            fraction E = new fraction(1, 1);
            if (C == E) z = true;
            Assert.IsTrue(z);
        }

六、重要优化

1.整数化为分母为1的分数,避免整数除法的误差;

2.在创建题目的查重过程中即进行计算,而不是读取题目的字符串后再进行计算,减少了额外的计算量。

3.修正了用户完成一次答题之后返回用户界面,再次点击Begin按钮时程序报错问题(索引超出数组范围),实现了用户重复点击Begin按钮后进行新一轮的答题的功能。

4.修正了超时后答题界面的题目更新问题。

5.加入查询用户历史记录时,若用户还未答题的提示

6.当进入用户主界面(MainGUI)时主界面入口窗口(MainInterface)隐藏,进入答题界面(PuzzlesGUI)或历史记录界面(RecordGUI)时用户主界面隐藏,当关闭两个子界面后用户主界面才会再次显示,优化了视觉体验。
7.为最终的exe文件更换了图标,过程详见我的另一篇博客:exe文件图标更换
8.修正了答题界面点击Cancel按钮之后倒计时没有立即停止而是进入下一轮计时的问题。
9.修正了重复点击Query按钮时历史记录重复生成的问题,改为Query按钮在点击一次后隐藏,直到下一次打开RecordGUI才会再次显示。

七、性能分析

在这里插入图片描述
在这里插入图片描述
引用队友博客中的性能分析:

由性能分析报告可知,题目生成和查重函数比较耗时,而题目生成的主要调用函数也是Check()函数。因此仅需要对查重函数Check()进行改进即可。

对其的改进为:设置一个题型标记数组Labels[],在判重时,优先判断题型是否一样,再判断解题过程的长度是否一样,最后再判断解题过程内容是否完全一样,这样可以减少判重时间。


八、最终成果运行展示

最终的exe可执行文件:Arithmetic.exe

1.主界面入口
主界面入口
2.用户主界面,不输入ID和生成题目数点击Begin和Record无效。
用户界面
核心代码:Begin点击事件Record点击事件

//--------------开始答题-----------
        private void btn_begin_Click(object sender, EventArgs e)
        {
            //将上一次的历史记录清空
            for (int i = 0; i< PuzzlesGUI.History.Length; i++)
            {
                PuzzlesGUI.History[i] = "";
            }
            PuzzlesGUI.score = 0;//清空上次答题分数
            tb_num.Focus();//鼠标自动定位到输入题目数的文本框
            num = 0;
            string N;
            user_name = tb_ID.Text;//获取用户输入
            N = tb_num.Text;

            //-------------检查输入是否合法 + 选择乘方模式-------------------
            CheckInput(N);            
            ChooseModel();
            
            if(user_name.Length == 0)
            {
                MessageBox.Show("Please enter your ID.");
            }
            else if(N.Length == 0 || N == "0")
            {
                MessageBox.Show("Please enter the number of puzzles.");
            }
            else if(N.Length >= 4 && N!= "1000")//保证题目数小于等于1000
            {
                MessageBox.Show("Number is too big!");
            }
            else if (CheckN == false)
            {
                MessageBox.Show("Error Input!\nPlease enter correct number.");
            }
            else if(CheckN == true)
            {
                num = Convert.ToInt32(N);
                //---------生成num道题目------------------- 
                n_puzzle.PuzzleGenerate(num, type);

                //----------弹出PuzzleGUI------------------
                this.Visible = false;
                PuzzlesGUI Puzzle = new PuzzlesGUI();
                Puzzle.ShowDialog();
                this.Visible = true;
            }
            
        }
//------------------查看历史记录-----------
        private void btn_record_Click(object sender, EventArgs e)
        {
            if(tb_ID.Text == "")
            {
                MessageBox.Show("Please enter your ID.");
            }
            else
            {
                this.Visible = false;
                tb_num.Focus();
                user_name = tb_ID.Text;
                RecordGUI Record = new RecordGUI();
                Record.ShowDialog();
                this.Visible = true;
                tb_num.Text = null;
                tb_num.Focus();
            }
            
        }

3.答题界面(点击begin之后弹出),右上角设有20秒的倒计时。

答题界面
未在20秒内答完题目,弹出超时的题目,并更新到下一道题,未做的题不计入答题历史记录。
倒计时Code

//-------------倒计时--------------------------------
        private void timer1_Tick(object sender, EventArgs e)
        {
            lab_time_num.Text = (seconds--).ToString() + " seconds";

            if (seconds == -1)//超时后弹出提示窗口,更新题目、题号、剩余题目数,,清空答案输入框,进入下一轮计时
            {
                timer1.Stop();
                lab_time_num.Visible = false;//倒计时显示关闭
                MessageBox.Show("Time Out!");
                lab_puzzle.Text = MainGUI.puzzle_str[++current];
                lab_count.Text = "No." + current; 
                lab_left_num.Text = lab_left_num.Text = Convert.ToString(MainGUI.num - current);
                tb_answer.Text = null;
                seconds = 20;
                timer1.Start();
                lab_time_num.Text = (seconds--).ToString() + " seconds";
                lab_time_num.Visible = true; //倒计时显示打开
            }
        }

超时
在这里插入图片描述
更新到下一题,重新开始计时
在这里插入图片描述
核心代码:点击OK事件

private void btn_ok_Click(object sender, EventArgs e)
        {
            tb_answer.Focus();
            timer1.Stop();//停止计时
            userAnswer = this.tb_answer.Text;//获取用户的输入
            ToTrueFraction(MainGUI.answer_str[current]);//调用存储的计算结果
           
            CheckAnswer();//核对用户答案
            this.tb_answer.Text = "";//将输入框清空
            History[current] = "NO." + current + ": " + MainGUI.puzzle_str[current] + Environment.NewLine 
                                + Environment.NewLine + IsTrue
                                + "  UserAnswer: " + userAnswer
                                + "  CorrectAnswer: " + result
                                + Environment.NewLine + Environment.NewLine;
                                //传递答题数据到历史记录
            lab_puzzle.Text = MainGUI.puzzle_str[++current];//更新PuzzleGUI的题目显示
            seconds = 20;//重置计时时间
            timer1.Start();//进行下一轮计时
            lab_count.Text = "No." + current;//更新题号
            IsTrue = false;
            lab_left_num.Text = Convert.ToString(MainGUI.num - current);
            if (current == MainGUI .num)//题目做完后,退出答题界面
            {
                timer1.Stop();
                current = 0;  //重置
                MainGUI.tb_num.Text = null;
                MessageBox.Show("All questions are completed!");
                this.Close();
            }
        }

答题错误,弹出错误提示
在这里插入图片描述
回答正确,弹出提示窗口,之后score+1。
在这里插入图片描述
问题全部答完后,剩余题目数为0,提示答题完毕并退出答题界面,返回到用户界面。
在这里插入图片描述
4.历史记录界面,点击Query查询上一次答题的历史记录,点击Quit退出历史记录界面,返回到用户界面。
核心代码:点击Query事件

        private void btn_query_Click(object sender, EventArgs e)
        {
            
            lab_user_name.Text = MainGUI.user_name;
            lab_score.Text = Convert.ToString(PuzzlesGUI.score);
            if(PuzzlesGUI.History[0] == null)
            {
                MessageBox.Show("You hanven't done questions yet!");
            }
            for(int i = 0; i < PuzzlesGUI .History.Length ; i++)
            {
                tb_record.Text += PuzzlesGUI.History[i];
            }            
        }

如果用户没有做一道题,那么点击查询之后提示用户未做题。
在这里插入图片描述
历史记录允许水平方向和竖直方向滚动。
在这里插入图片描述

九、PSP表格实际耗时

PSP2.1Personal Software Process Stages预估耗时(min)实际耗时(min)
Planning计划2020
Estimate估计这个任务需要多少时间2020
Development开发28802160
Analysis需求分析(包括学习新技术)14401460
Design Spec生成设计文档00
Design Review设计复审(和同事审核设计文档)00
Coding Standard代码规范(为目前的开发制定合适的规范)00
Design具体设计6060
Coding具体编码57605760
Code Review代码复审60120
Test测试(自我测试,修改代码,提交修改)10002000
Reporting报告00
Test Report测试报告600
Size Measurement计算工作量600
Postmortem & Process Improvement Plan事后总结,并提出过程改进计划60180
合计1136011900

十、总结

在本次结对项目中,我们一开始的分工是将项目划分为生成题目和计算题目两个模块,但随着项目推进,发现生成题目过程中若要保证题目不重复就要进行查重,即需要对题目进行计算,此时我已经写好了计算题目的代码,无疑这样就是将计算过程进行了两次,造成了大量的计算量重叠。
  并且,我一开始将题目分为整数和分数运算时,没有考虑到整数题目的除法会造成误差,精确的结果应当用分数来表示。因而最优的计算应当是一开始就将数据封装为分数形式的类。
于是最终的结果是我的组伴完成了生成题目、计算题目的核心代码部分,我则主要负责C#界面和功能设计、优化以及部分单元测试。
总结之后,发现前两天的C++代码花费了我大量时间是非常不划算且完全没有意义。合理的分工应当是,一个人写C++题目生成和计算,另一个人去完成C#界面设计
  当然也有很多收获,比如从对C#从陌生到可以做出简单的exe可执行文件,亲眼看到一个简单软件在你手中成形是非常有成就感的。

1…心得:
  进行C#(winform)界面设计时,在不同窗口之间传递值要将那个值设为public static,并且一定要注意button_Click()事件里的数据重置清零的问题。
  进行timer计时时,一定要注意start()和stop()d的时机,防止进入反复计时。
  C#里只有int,没有long long int,因为C#里的int表示的范围和C++里的long long int一样大。
  C++的栈里栈顶元素表示为stack.top(),是可以直接对其赋值的,允许这样的操作(stack.top() = 12;),但C#的栈顶元素stack.Peek()不允许为其直接赋值,要改变栈顶元素的值只能在Pop()之后再Push()。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

茶如影

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值