使用Python和 PyQt5 实现简单可视化计算器

课程名称软件工程 :https://bbs.csdn.net/forums/ssynkqtd-05
作业要求https://bbs.csdn.net/topics/617294583
作业目标实现具有基本功能和科学功能的可视化计算器
参考资料https://blog.csdn.net/People1007/article/details/124722588

Gitcode项目地址

使用Python和 PyQt5 实现简单可视化计算器

功能展示

声明

1.本程序不处理隐藏符号,请像写C语言一样清晰地写出表达式。
2.本程序只会补足三角函数、log、ln的左括号,右括号需要自行添加,否则会导致判断错误。
3.本程序的三角函数处理弧度制。

基本功能展示

Alt

附加功能展示

请添加图片描述

PSP表格

PSPPersonal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning计划53
• Estimate• 估计这个任务需要多少时间53
Development开发5851381
• Analysis• 需求分析 (包括学习新技术)120300
• Design Spec• 生成设计文档58
• Design Review• 设计复审55
• Coding Standard• 代码规范 (为目前的开发制定合适的规范)53
• Design• 具体设计60125
• Coding• 具体编码240400
• Code Review• 代码复审120360
• Test• 测试(自我测试,修改代码,提交修改)30180
Reporting报告3545
• Test Repor• 测试报告1020
• Size Measurement• 计算工作量1015
• Postmortem & Process Improvement Plan• 事后总结, 并提出过程改进计划1510
合计6251429

解题思路

问题1 确定语言

在Java和Python中纠结,Java是一直想学习的语言,Python略有接触,懂得最基本的语法知识,考虑到时间和完成度等因素,最后选择接触得比较多的Python语言。

问题2 GUI界面设计

通过网络搜索发现PyQt5和Tkinter是Python的GUI界面设计常用的库。
Tkinter的优点是简单易用,上手快,对于我这种临时学习选手是最优选择,但它也存在一个问题,就是设计效果比PyQt5简陋。
PyQt5虽然设计效果更华丽,但经过查找资料,可以发现它的教程和指南的数量比 Tkinter 要少得多,且代码更难调试。
拿到题目后就想做一个粉色的计算器,比起追求难度的简易更希望可以做出自己想象中的东西!Tkinter的设计效果可能没办法满足我的想象,所以最终选择PyQt5对可视化界面进行设计,虽然在设计过程中因为操作不熟练磕磕绊绊心态多次崩溃,但最终的结果还是较为满意。

问题3 代码思路

首先计算器需要实现加、减、乘、除、归零基本操作,这个基础要求让我想起在数据结构这门课中被后缀表达式计算支配的恐惧,仔细思考后决定以后缀表达式的计算为基础进行编程。
比较棘手的是附加功能,次方、幂、三角函数有不同的符号表示,该怎么在字符串中识别执行的功能并计算呢?
假设在按下“=”后,程序将对输入的字符串进行处理。它会分别计算执行不同操作的算式,并将计算结果代替原算式在字符串中的位置。然后,程序将递归地处理新的算式,直到算式中只包含加、减、乘、除这些基本操作,如下所示:
s i n ( 30 ) + 2 2 − c o s ( 58 ) ↓ − 0.98803162409 + 4 − 0.11918013545 sin(30)+2^2-cos(58) \\\downarrow \\-0.98803162409+4-0.11918013545 sin(30)+22cos(58)0.98803162409+40.11918013545

设计与实现过程

GUI设计

在Qt Designer中,我使用粉色作为主色调来设计计算器的可视化界面,这里选取的粉色灵感来源于小猪佩奇粉,结果意外的不错。按键的内容和功能设置如下图所示:
在这里插入图片描述
初版设计的界面上半部分显得有些单调,粉色让我想起最近上映的电影《芭比》海报的字体设计,所以我加入粉色的水印元素,以此为设计亮点。
在这里插入图片描述
当用户将鼠标悬停在按钮上时,按钮会变成灰色,这样可以帮助用户确认他们所点击的按钮是正确的。

在这里插入图片描述

代码设计

这段代码主要定义一个名为 Calculator 的类,它包含计算器的各种功能实现。
1. __ init __()
在初始化UI界面的过程中,将程序与按钮进行关联,确保按钮功能能够正常响应。
2. buttonClick()
按下按钮时,相应的符号将会出现在输入框中。符号能否出现主要判断有两个方面
①在计算机处于初始状态时是否可以输入特定符号
②检查前一个输入的符号,确保新输入的符号在这种情况下是允许的。
例如,如果前一个符号是“π”、“e”或“)”时,就不允许输入数字“0”。
数字“0”输入规则相关代码如下:

if but.objectName()=="But_0":
    text = self.Text.text()
    if text == "0" or text == "0.0":  # 若计算器初始化为0,如若处于初始化状态可直接替换
        text = "0"
    elif not (text[-1] == 'π' or text[-1] == 'e' or  text[-1] == ')'):
        text += "0"
    else:
        reply = QMessageBox.critical(self, '标题', '<h2>操作不符合要求,请重新输入</h2>', QMessageBox.Yes | QMessageBox.No,
                                     QMessageBox.Yes)
    self.Text.setText(text)  # 将更改后的文本写入在文本编辑框,显示出来

依次类推,对每一个输入符号进行讨论。如果它不符合上述两条规则中的任何一条,就会触发错误。
在这里插入图片描述
当点击“=”按钮时,将对输入的字符串进行处理和运算。
3. match()
按下"="按钮后,会检查输入的字符串中的括号使用是否符合表达式的要求,左右括号的数量是否相等。

def match(self,string):
    text=string
    num = text.count('(') - text.count(')')
    if num!=0:
        return 1
    return 0

4. check_point()
通过计算小数点的数量来检查输入的字符串中小数点的使用是否符合规范,例如,是否出现类似1.2.3这样错误的格式。如果表达式中存在这种错误,计算器将会报错。
在这里插入图片描述
5. char_judge()
这个函数的主要目的是解决连续输入 e 和 π 的情况,并在接下来输入数字或函数时避免出错,例如避免输入 etan 这样的情况。以下是相应的代码:

def char_judge(self,string):
    setwrong = {'πe', 'eπ', 'ππ', 'ee', 'πt', 'πs', 'πc', 'πl', 'et', 'es', 'ec', 'el', 'πr', 'er'}
    setwrong2 = {'π0', 'π1', 'π2', 'π3', 'π4', 'π5', 'π6', 'π7', 'π8', 'π9', 'e1', 'e0', 'e2', 'e3', 'e4', 'e5',
                 'e6', 'e7', 'e8', 'e9'}
    for i in setwrong:
        if i in string:
            return True
    for i in setwrong2:
        if i in string:
            return True
    return False

6. cal_replace()
cal_replace 函数的主要目的是对输入的表达式进行预处理,主要包括先计算各个函数的值,将表达式中的函数替换为它们的计算结果,从而简化表达式,不再包含各个函数的调用。
7. regulmatch()
正则表达式用于匹配数字。

def regulmatch(self,ch):
    return re.match("-?\\d+\\.*\\d*", ch)

8. priority()
定义运算符的优先级以辅助后缀表达式的计算。

def priority(self,str1):
    n = 0
    if str1 == "+" or str1 == "-":
        n = -1
    if str1 == "*" or str1 == "÷":
        n = 1
    if str1 == "(":
        n = -2
    if str1 == ")":
        n = 2
    return n

9. place_space()
将字符串中的不同输入符号用空格分隔开,以便于后续对字符串中函数的计算和处理。

def place_space(self,str):
    text=str
    text = text.replace('sinh', 'sinh ')
    text = text.replace('cosh', 'cosh ')
    text = text.replace('tanh', 'tanh ')
    text = re.sub(r'\bsin\b', 'sin ', text)
    text = re.sub(r'\bcos\b', 'cos ', text)
    text = re.sub(r'\btan\b', 'tan ', text)
    text = text.replace('^', ' ^ ')
    text = text.replace('log', 'log ')
    text = text.replace('(', '( ')
    text = text.replace(')', ' )')
    text = text.replace('ln', 'ln ')
    text = text.replace('*', ' * ')
    text = text.replace('÷', ' ÷ ')
    text = text.replace('+', ' + ')
    text = text.replace('-', ' - ')
    text = text.replace("  "," ")
    return text

10. calculate()
计算后缀表达式以获得结果。在这一阶段,处理的字符串中只包含加减乘除这些运算符号。
11. compute()
遍历到操作符时压栈的计算方法。

def compute(self,num1,num2,sign):
    result = 0
    if sign == "+":
        result = num1 + num2
    if sign == "-":
        result = num2 - num1  # 接近栈底的是被减数
    if sign == "*":
        result = num1 * num2
    if sign == "÷":
        if num1 != 0:
            result = num2 / num1  # 接近栈底的是被除数
    return result

关键代码展示

calculate()

表达式求值的核心模块是calculate(),这段代码利用栈的思想来处理包含括号的数学表达式,根据运算符的优先级进行计算,最终得到表达式的计算结果。
1.calculate 函数接受一个字符串 str 作为输入,该字符串包含需要计算的数学表达式。
2.value 和 operator 分别用于存放数字和运算符。
3.通过一个 while 循环遍历输入的字符串 text。
4.在循环内部

  • 首先判断当前字符是否为空格,如果是则直接跳过,继续下一个字符。
  • 如果当前字符是减号 “-”,则判断它是否是负号,即它前面是左括号或者它是表达式的第一个字符。如果是负号,就找到负号后面的数字,并将负号和数字组合成一个负数添加到 value 中。
  • 如果当前字符是数字(包括整数和小数),则找到它的所有连续数字,并将它们转换成浮点数添加到 value 中。
  • 如果当前字符是左括号 “(”, 则将它添加到 operator 中。
  • 如果当前字符是右括号 “)”, 则将 operator 中的运算符逐个弹出,对相应的数字进行计算,然后将计算结果重新添加到 value 中,直到遇到左括号。
  • 如果当前字符是加减乘除符号,则判断它的优先级是否高于栈顶的运算符,如果是,则将当前运算符添加到 operator 中。否则,将栈顶的运算符弹出,对相应的数字进行计算,然后将计算结果重新添加到 value 中,直到当前运算符的优先级高于栈顶的运算符。

6.循环结束后,还需要对剩余的运算符进行计算。
7.最终返回计算结果。

def calculate(self,str):
	text = str
	value=[] #存放数字
	operator=[] #装符号
	i=0
	while i <len(text):
	    if text[i]==' ':
	        i+=1
	        continue
	
	    if text[i]=='-':
	        if text[i-1] == '(' or i == 0:
	            j = i+1
	            while j < len(text) and text[j] in "0123456789.":
	                j += 1
	            if j != i:
	                value.append(float(text[i:j]))
	            i=j
	
	    j = i
	    while j < len(text) and text[j] in "0123456789.":
	        j += 1
	
	    if j != i:
	        value.append(float(text[i:j]))
	
	    i = j
	    if i >= len(text):
	        break
	
	    if operator==[]:
	        operator.append(text[i])
	        i+=1
	    elif text[i]=='(':
	        operator.append(text[i])
	        i += 1
	    elif text[i]==')':
	        while not (operator[-1] == "("):
	            num1 = value.pop()
	            num2 = value.pop()
	            result=self.compute(num1, num2, operator[-1])
	            operator.pop()
	            value.append(result)
	        operator.pop()
	        i += 1
	    elif text[i] in '+-*÷':
	        while operator and self.priority(text[i]) <= self.priority(operator[-1]) :
	            num1=value.pop()
	            num2=value.pop()
	            result=self.compute(num1,num2,operator[-1])
	            value.append(result)
	            operator.pop()
	        operator.append(text[i])
	        i += 1
	
	while operator:
	    if len(value) > 1:
	        num1 = value.pop()
	        num2 = value.pop()
	        result=self.compute(num1, num2, operator[-1])
	        value.append(result)
	        operator.pop()
	    elif len(value) == 1 and len(operator)==1:
	        value.append(-1*(value.pop()))
	        operator.pop()
	
	return round(float(value[0]),4)

cal_replace()

cal_replace 函数实现对输入字符串中特定部分的识别和替换,以便后续的计算操作。它考虑一些特定的函数和运算符,并根据它们的位置和参数进行相应的计算和替换。同时,代码中也包含一些异常处理的部分,以保证程序的稳定性。
1.首先,它将输入字符串中的连续多个空格替换成单个空格,保证字符串中单词之间只有一个空格。
2.然后,它将处理后的字符串按照空格进行分割,得到一个列表 expression,里面包含表达式中的各个单词(或符号)。
3.接着,定义一个标志 flag,用来记录表达式中是否有替换的地方,以解决函数嵌套的问题。
4.定义一个集合 set1,包含可能需要进行函数值计算的函数名。
5.使用一个循环遍历 expression 列表。
6.在循环中,首先判断是否有 “e”,如果有,则将其替换为数学常数 math.e。
7.接着,判断是否有 “π”,如果有,则将其替换为数学常数 math.pi。
8.然后,根据一些特定的函数名和它们的位置,进行相应的计算和替换操作。例如:

  • 对于 “^” 运算符,判断是否可以进行幂运算,并进行相应的计算和替换。
  • 对于 “log” 函数,计算对数并进行替换。
  • 对于三角函数和双曲函数,计算相应的值并进行替换。
  • 对于自然对数 “ln”,计算对数并进行替换。

9.最后,返回经过处理后的字符串。
因为代码过长,这里仅展示一小部分,可以在Gitcode中查看完整代码。

def cal_replace(self,string):
	string = string.replace('  ', ' ')
	expression = string.split(" ")  # 用空格分割字符串
	flag = 0  # 这个标志用来记录表达式中是否有替换的地方,用以解决函数嵌套问题,例如ln ln 5的情况
	set1 = {'sin', 'cos', 'tan', 'sinh', 'cosh', 'tanh','^'}
	
	try:
	    for i in range(len(expression)):
	        if expression[i] == "e":  # 以下内容基本上都是求函数值,比如将表达式中的sin30替换成0.5
	            result = math.e
	            string = string.replace('e', str(result))
	            flag = 1
	        if expression[i] == "π":
	            result = math.pi
	            string = string.replace('π', str(result))
	            flag = 1
	        if expression[i] in set1 and expression[i + 1] in '-' and self.regulmatch(expression[i + 2]):
	            string = string.replace(expression[i + 1] + ' ' + expression[i + 2],
	                                    str(float(expression[i + 2]) * (-1)))
	            flag = 1
	            ······

程序性能改进

添加 find_wrong 函数,该函数改进点主要在于错误提示的完善以及对输入情况的细致处理。
最初的错误提示是“操作不符合要求,请重新输入”,现在通过添加 find_wrong 函数,错误提示变得更加详细,可以具体指出出错的地方。
在处理输入时,原先主要是按符号进行讨论,现在引入按照不同情况进行讨论的方法,确保用户在输入时能够得到正确的反馈和结果。
该函数主要检测一些比较容易描述的典型错误,如:

  • 对负数的输入格式进行规范,要么输入为0-x的形式,或者写成(-x)。
    在这里插入图片描述

  • "*+.)÷"不能出现在表达式最前面,可能会因为初始化字符串设为“0”而不会出现这种情况,但仍然进行相应的检测。

  • "±*÷.^"不能出现在表达式最后面
    在这里插入图片描述

  • e和π输入可能出错的情况,比如“ee”,“e9”等
    在这里插入图片描述

  • 检查是否有连续输入运算符的情况,如++,–
    在这里插入图片描述

  • 检查除数是否为0
    在这里插入图片描述

  • 判断tan的取值是否有问题
    在这里插入图片描述

单元测试展示

使用Python的unittest模块对程序进行全面的单元测试。首先,将Caculator.py中用于计算的函数提取出来,摒弃可视化代码部分,组成test_calculate.py文件,针对计算器的每个功能编写相应的单元测试方法,逐一测试不同功能的正确性。
举例来说,test_plus方法测试加法功能。通过使用self.assertEqual来断言计算器对于输入的表达式能够得到预期的结果。而test_sin、test_cos、test_tan等方法测试三角函数的功能,使用self.assertAlmostEqual来断言计算器的输出在一定误差范围内等于预期值。
在运行测试脚本时,unittest会自动执行所有的测试方法,并输出测试结果。如果所有测试通过,将会看到一个成功的提示;如果有测试失败,将会显示哪些测试失败以及失败的原因。
单元测试代码如下:

import unittest
import math
from test_calculate import test

class TestCalculator(unittest.TestCase):

    def test_plus(self):
        self.assertEqual(test("3+5"), 8)
        self.assertEqual(test("-2+2"), 0)
        self.assertEqual(test("0+0"), 0)

    def test_re(self):
        self.assertEqual(test("5-3"), 2)
        self.assertEqual(test("2-5"), -3)
        self.assertEqual(test("0-0"), 0)

    def test_mul(self):
        self.assertEqual(test("3*5"), 15)
        self.assertEqual(test("-2*2"), -4)
        self.assertEqual(test("0*5"), 0)

    def test_div(self):
        self.assertEqual(test("10÷2"), 5)
        self.assertEqual(test("8÷2"), 4)
        self.assertEqual(test("5÷2"), 2.5)

    def test_sin(self):
        self.assertAlmostEqual(test("sin(30)"), 0.5)
        self.assertAlmostEqual(test("sin(45)"), math.sin(math.radians(45)),places=4)
        self.assertAlmostEqual(test("sin(60)"), math.sin(math.radians(60)),places=4)

    def test_cos(self):
        self.assertAlmostEqual(test("cos(60)"), 0.5)
        self.assertAlmostEqual(test("cos(45)"), math.cos(math.radians(45)),places=4)
        self.assertAlmostEqual(test("cos(30)"), math.cos(math.radians(30)),places=4)

    def test_tan(self):
        self.assertAlmostEqual(test("tan(45)"), 1.0)
        self.assertAlmostEqual(test("tan(60)"), math.tan(math.radians(60)),places=4)
        self.assertAlmostEqual(test("tan(30)"), math.tan(math.radians(30)),places=4)

    def test_xy(self):
        self.assertAlmostEqual(test("2^(3)"), 8.0)
        self.assertAlmostEqual(test("3^(2)"), 9.0)
        self.assertAlmostEqual(test("2^(0.5)"), math.sqrt(2),places=4)

    def test_x2(self):
        self.assertAlmostEqual(test("4^(2)"), 16.0)
        self.assertAlmostEqual(test("0.5^(2)"), 0.25)

    def test_sinh(self):
        self.assertAlmostEqual(test("sinh(0)"), 0.0)
        self.assertAlmostEqual(test("sinh(1)"), math.sinh(math.radians(1)),places=4)
        self.assertAlmostEqual(test("sinh(2)"), math.sinh(math.radians(2)),places=4)

    def test_cosh(self):
        self.assertAlmostEqual(test("cosh(0)"), 1.0)
        self.assertAlmostEqual(test("cosh(1)"), math.cosh(math.radians(1)),places=4)
        self.assertAlmostEqual(test("cosh(2)"), math.cosh(math.radians(2)),places=4)

    def test_tanh(self):
        self.assertAlmostEqual(test("tanh(0)"), 0.0)
        self.assertAlmostEqual(test("tanh(1)"), math.tanh(math.radians(1)),places=4)
        self.assertAlmostEqual(test("tanh(2)"), math.tanh(math.radians(2)),places=4)

    def test_ln(self):
        self.assertAlmostEqual(test("ln(e)"), 1.0)
        self.assertAlmostEqual(test("ln(e^(2))"), 2.0)
        self.assertAlmostEqual(test("ln(e^(3))"), 3.0)

    def test_log(self):
        self.assertAlmostEqual(test("log(1)"), 0.0)
        self.assertAlmostEqual(test("log(100)"), 2.0)
        self.assertAlmostEqual(test("log(1000)"), 3.0)

if __name__ == '__main__':
    unittest.main()

测试覆盖率如下:
在这里插入图片描述
覆盖率是指在测试中覆盖到的代码或功能的比例。提高覆盖率可以帮助你发现更多的代码错误和潜在的问题,从而提高软件的质量和稳定性。以下是一些优化覆盖率的方法:

  • 制定清晰的测试计划,确定测试的目标和范围,明确要覆盖的功能和模块。
  • 选择合适的测试框架,根据项目的需求选择合适的测试框架,例如JUnit、PyTest等。
  • 编写有效的测试用例,确保每个测试用例都是独立的,可以单独执行,而且具有清晰的输入和预期输出。

心路历程与收获

1.对于 PyQt5 的强大功能有了一定的了解,尤其是通过观看B站上的教学视频,看到一些华丽的界面设计,让我感叹自己的能力还有待提高。虽然只学会一些基础功能,但希望能在未来深入学习 PyQt5,掌握更高级的技能!
2.重新学习后缀表达式的计算方法,在编写嵌套递归的代码时,虽然耗费许多脑力,但也锻炼了我的思维能力。不断地犯错、修改,培养了我耐心和解决问题的能力。
3.通过这次软件工程作业,我深刻体验到它的难度。从PSP表格中可以看出,实际所需时间远远超过预估时间。主要原因在于我对自己的实力缺乏清晰的认知,而且这也是我第一次做这样的作业,还不够熟练。希望下次能做得更出色!

  • 4
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
以下是一个简单PyQt5 计算器实现: ```python from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QPushButton, QLineEdit import sys class Calculator(QMainWindow): def __init__(self): super().__init__() # 设置窗口标题和大小 self.setWindowTitle('Calculator') self.setFixedSize(240, 320) # 创建中心窗口和布局 self.central_widget = QWidget() self.setCentralWidget(self.central_widget) self.central_layout = QVBoxLayout() self.central_widget.setLayout(self.central_layout) # 创建文本框和按钮 self.display = QLineEdit() self.display.setReadOnly(True) self.central_layout.addWidget(self.display) self.buttons = [] button_layout = QGridLayout() button_layout.setSpacing(5) for i in range(0, 10): button = QPushButton(str(i)) button.clicked.connect(self.button_clicked) button_layout.addWidget(button, int((9-i)/3), (i-1)%3) self.buttons.append(button) self.add_button = QPushButton('+') self.add_button.clicked.connect(self.button_clicked) button_layout.addWidget(self.add_button, 0, 3) self.sub_button = QPushButton('-') self.sub_button.clicked.connect(self.button_clicked) button_layout.addWidget(self.sub_button, 1, 3) self.mul_button = QPushButton('*') self.mul_button.clicked.connect(self.button_clicked) button_layout.addWidget(self.mul_button, 2, 3) self.div_button = QPushButton('/') self.div_button.clicked.connect(self.button_clicked) button_layout.addWidget(self.div_button, 3, 3) self.dot_button = QPushButton('.') self.dot_button.clicked.connect(self.button_clicked) button_layout.addWidget(self.dot_button, 3, 2) self.eq_button = QPushButton('=') self.eq_button.clicked.connect(self.button_clicked) button_layout.addWidget(self.eq_button, 3, 1) self.clear_button = QPushButton('C') self.clear_button.clicked.connect(self.clear_display) button_layout.addWidget(self.clear_button, 3, 0) self.central_layout.addLayout(button_layout) def button_clicked(self): sender = self.sender() if sender in self.buttons: self.display.setText(self.display.text() + sender.text()) elif sender == self.dot_button: if '.' not in self.display.text(): self.display.setText(self.display.text() + '.') elif sender == self.add_button: self.display.setText(self.display.text() + '+') elif sender == self.sub_button: self.display.setText(self.display.text() + '-') elif sender == self.mul_button: self.display.setText(self.display.text() + '*') elif sender == self.div_button: self.display.setText(self.display.text() + '/') elif sender == self.eq_button: try: result = eval(self.display.text()) self.display.setText(str(result)) except: self.display.setText('Error') def clear_display(self): self.display.setText('') if __name__ == '__main__': app = QApplication(sys.argv) calculator = Calculator() calculator.show() sys.exit(app.exec_()) ``` 这个计算器有基本的加减乘除、小数点和等于按钮,并且能够处理异常情况。运行这个程序将会出现一个简单计算器界面。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值