前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Pytest+Allure+Jenkins接口自动化项目实战(一)

Pytest+Allure+Jenkins接口自动化项目实战(一)

作者头像
王大力测试进阶之路
发布2020-02-19 16:31:14
3.2K0
发布2020-02-19 16:31:14
举报
文章被收录于专栏:橙子探索测试

经过一周多时间,基于python+pytest+excel+allure框架的接口自动化测试初版已基本实现,包括基本配置读取、用例读取、用例执行、sql读取执行、前置数据准备、后置数据清理以及测试报告生成等,环境独立运行、项目独立运行、用例独立运行、jenkins集成、邮件发送暂未实现,再后期版本会再次推出,现在把整个框架设计思路和想法分享给大家来参考和借鉴。希望大家也能提供更好的思路和方法帮助我进行优化改进。整个过程中遇到的问题清参考Python自动化测试疑问及解决方案(一)Python自动化测试|如何解决前置模块及数据依赖(二)

实战项目是三端交互的项目,所以在设计思路上要考虑多项目如何交互,目前只写了1个项目的,其它2个项目都预留了位置,后期直接添加就可以,思路一样。

项目实战涉及到的文章请参考:

【Pytest篇】Allure生成漂亮的HTML图形化测试报告(一))

Pytest框架集成Allure定制测试报告详解(一)

Pytest标记用例失败之xfail

Pytest跳过执行之@pytest.mark.skip()详解

Pytest自定义标记mark及指定文件/类/方法/用例执行

Pytest fixture之request传参

Pytest@pytest.mark.parametrize一键生成接口正交试验用例

Pytest脚本中用例运行方式

【Pytest篇】装饰器@pytest.mark.parametrize多样参数化(二)

Pytest装饰器@pytest.mark.parametrize数据驱动(三)

Python Pytest装饰器@pytest.mark.parametrize详解

Python Pytest中fixture之yield唤醒teardown和终结函数addfinalizer

Pytest之@pytest.mark.usefixtures()、叠加usefixtures、(autouse=True)详解

Python Pytest前置setup和后置teardown详解

Pytest全局用例共用之conftest.py详解

Python pytest框架之@pytest.fixture()和conftest详解

Python日志处理logging模块详解

Python读写yaml文件

Python之Pymysql模块实现MySQL增删改查

Python日志处理logging模块详解

一、整个代码目录及介绍

common

request.py 封装post、get请求方法,供所有地方调用

login.py 封装各项目、各种方式的登录方法,供所有地方调用

readFile.py 封装读取yaml中配置文件、excel中测试用例方法,供所有地方调用

execSql.py 封装sql操作增、删、改、查方法,供所有地方调用

prefixSqlData.py 封装前置、后置公共sql,供所有地方调用

assertion.py 封装断言方法,供所有用例中调用

logs.py 封装日志方法,供所有地方调用

config

sql.yaml 前置数据sql语句、后置清理sql语句等

test.yaml test环境数据库配置、接口域名、测试账号、登录接口数据等

uat.yaml uat环境数据库配置、接口域名、测试账号、登录接口数据等

testcase.xls 测试用例等

testcase

项目1

用例1、用例2

项目2

用例1、用例2

testcase.xls 接口测试用例等

conftest.py 放置了登录获取token供所有用例调用

run_all_case.py 执行所有测试用例并生成测试报告

logs

每天的日志数据

report

html 测试报告index.html

二、yaml文件基本配置

一些基本配置可以放到yaml文件里(数据库配置、接口域名、测试账号、登录接口数据)方便随时修改和读取

Python读写yaml文件请参考这篇文章。

代码语言:javascript
复制
项目一:
   url: 'https://www.baidu.com/'
   headers:
        Content-Type: 'application/json'
        Authorization: 'token'
   account:
     a_account: '17900000000'
     b_account: '18000000000'
     c_account: '19900000000'
     c_account1: '19900000001'
 
   c_login:
     method: post
     url: '/user/login'
     param:
        type: "7"
        login_from: 7
        mobile: "18888888888"
        code: "123456"
   c_sms_code:
     method: post
     url: '/1.0/users/login'
 
 
 
   mysql:
     host: 127.0.0.1
     user: test
     pwd: test
     test_db: user
 
 
 
项目二:
  url: 'https://www.baidu.com/'

三、yaml文件sql配置

Python读写yaml文件请参考这篇文章。

代码语言:javascript
复制
项目一:
  查id:
    - select_list
    - select id from A.B where create_mobile={}
  查用户id:
    - select_list
    - select id from A.B where mobile={}
  查团队id:
    - select_list
    - select id from A.B where company_user_id=(select id from A.B where mobile={})
  查C端用户id:
    - select_list
    - select user_id from A.B where mobile in({})
  解除用户:
    - update
    - update A.B set status=2 where mobile={}
 
项目二:
  查id:
    - select_list
    - select id from A.B where create_mobile={}
  查用户id:
    - select_list
    - select id from A.B where mobile={}
  查团队id:
    - select_list
    - select id from A.B where company_user_id=(select id from A.B where mobile={})
  查C端用户id:
    - select_list
    - select user_id from A.B where mobile in({})
  解除用户:
    - update
    - update A.B set status=2 where mobile={}

四、读取yaml文件、excel用例文件

Python读写yaml文件请参考这篇文章。

代码语言:javascript
复制
#!/usr/bin/env python
# _*_coding:utf-8_*_
import yaml,os,sys,xlwt,xlrd
from common.logs import Log
 
class ReadFile(object):
    log = Log()
    _instance=None
    def __new__(cls,*args,**kwargs):
        if cls._instance is None:
            cls._instance=super().__new__(cls)
        return cls._instance
 
    def __init__(self):
        self.excel_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config/testcase.xls')
        self.yaml_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config/test.yaml')
        self.sql_yaml_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config/sql.yaml')
 
    def read_yaml(self,path_type):
        """
        读yaml文件
        :return:
        """
        try:
            if path_type=='yaml_path':
                file_path=self.yaml_path
            elif path_type=='sql_yaml_path':
                file_path=self.sql_yaml_path
 
            with open(file_path,'r',encoding='utf-8') as f:
                return yaml.load(f.read())
        except Exception as e:
            self.log.error("读yaml文件报错{}".format(e))
 
    def read_excel(self,sheet_name,function,casename=None):
        """
        读取excel
        :param sheet_name:
        :param function:
        :return:
        """
        try:
            book=xlrd.open_workbook(self.excel_path)
            sheet=book.sheet_by_name(sheet_name)
            param=[]
            for i in range(0,sheet.nrows):
                if casename==None:
                    if sheet.row_values(i)[0]==function and sheet.row_values(i)[3]==1:
                        param.append(sheet.row_values(i))
                else:
                    if sheet.row_values(i)[0]==function and sheet.row_values(i)[1]==casename and sheet.row_values(i)[3]==1:
                        param.append(sheet.row_values(i))
            return param
        except Exception as e:
            self.log.error("读取excel报错{}".format(e))
 
 
if __name__ == '__main__':
    test=ReadFile()
    print(test.read_excel('lxk','我的','全部页面'))

五、用例模板

根据每个sheet存放不同项目的测试用例,然后根据再根据sheet去读取对应项目模块的测试用例

Function模块、CaseName测试用例名、Type请求类型、Run是否执行、URL接口地址、Headers请求头、Param请求参数、SQL1、SQL2、SQL3测试中需用到的前置数据或后置数据、AssertType断言类型,因为接口返回的响应数据可能会多种多样,所以这里断言分了几种情况、Expect1预期结果1、Expect2预期结果2、Expect3预期结果3

六、request方法

代码语言:javascript
复制
#!/usr/bin/env python
# _*_coding:utf-8_*_
import requests,urllib3
from urllib3 import encode_multipart_formdata
from common.logs import Log
 
class RunMethod(object):
    """
    request
    """
    log = Log()
    urllib3.disable_warnings()
 
 
    def post_main(self,url,data,header,file=None):
        """
        post请求
        :param url:
        :param data:
        :param header:
        :param file:
        :return:
        """
        res=None
        if file!=None:
            res=requests.post(url=url,json=data,headers=header,verify=False)
        else:
            res = requests.post(url=url, json=data,headers=header, files=file, verify=False)
        return res.json()
 
    def get_main(self,url,header,param=None):
        """
        get请求
        :param url:
        :param header:
        :param param:
        :return:
        """
        res=None
        if param!=None:
            res=requests.get(url=url,headers=header,verify=False)
        else:
            res = requests.get(url=url, headers=header, json=param,verify=False)
        return res.json()
 
    def run_main(self,method,url,header,data=None,file=None):
        """
        被调用主request
        :param method:
        :param url:
        :param header:
        :param data:
        :param file:
        :return:
        """
        try:
            res=None
            if method=='post' or method=='POST' or method=='Post':
                res=self.post_main(url,data,header,file=None)
            elif method=='get' or method=='GET' or method=='Get':
                res=self.get_main(url,header,param=None)
            else:
                return "request传参错误"
            return res
        except Exception as e:
            self.log.error("请求方法报错{}".farmat(e))
if __name__ == '__main__':
    print(111)

七、登录方法

代码语言:javascript
复制
#!/usr/bin/env python
# _*_coding:utf-8_*_
from common import request
from common.readFile import ReadFile
from common.logs import Log
 
 
class Login(object):
    """
    登录
    """
    log = Log()
    request = request.RunMethod()
 
    def __init__(self):
        self.yaml_data = ReadFile().read_yaml('yaml_path')['lxk']
        self.header = self.yaml_data['headers']
        self.url = self.yaml_data['url']
        self.lxk_c_url = self.yaml_data['c_login']['url']
        self.lxk_c_method = self.yaml_data['c_login']['method']
        self.lxk_c_param = self.yaml_data['c_login']['param']
 
 
    def lxk_c_login(self,project,mobile):
        """
        蓝薪卡C端登录
        :param project:
        :param mobile:
        :return:
        """
        try:
            if project=='lxk_c':
                self.lxk_c_param['mobile']=mobile
                result=self.request.run_main(self.lxk_c_method, self.url+self.lxk_c_url, self.header, self.lxk_c_param)
            elif project=='lxk_a':
                pass
            elif project=='lxk_b':
                pass
            return result
        except Exception as e:
            self.log.error('登录报错{}'.format(e))
 
if __name__ == '__main__':
    test=Login()
    print(test.lxk_c_login('lxk_c','18221124104'))

八、操作sql方法

Python之Pymysql模块实现MySQL增删改查请参考这篇文章

代码语言:javascript
复制
#!/usr/bin/env python
# _*_coding:utf-8_*_
from common.readFile import ReadFile
import pymysql
import sys
from common.logs import Log
 
 
 
class ExecSql(object):
    """
    执行sql语句类
    """
    log = Log()
 
    _instance=None
    def __new__(cls,*args,**kwargs):
        if cls._instance is None:
            cls._instance=super().__new__(cls)
        return cls._instance
 
    def __init__(self):
        """
        初始化mysql配置
        :param platform_name:
        """
        #self.sql_conf = self._get_sql_conf(platform_name)
        self.sql_conf=None
 
    def _get_sql_conf(self, project):
        """
        获取mysql配置
        :param platform_name:
        :return:
        """
        try:
            return ReadFile().read_yaml('yaml_path')[project]['mysql']
        except:
            self.log.error("找不到对应项目:{0}".format(project))
 
    def connect_db(self):
        """
        连接mysql
        :return:
        """
        host = self.sql_conf['host']
        user = self.sql_conf['user']
        pwd = self.sql_conf['pwd']
        test_db = self.sql_conf['test_db']
        try:
            self.conn = pymysql.connect(host=host, user=user, password=pwd, db=test_db, port=3306, charset="utf8")
        except Exception as e:
            self.log.error("连接mysql失败:{0}".format(e))
 
    def get_cursor(self):
        """
        获取游标
        :return:
        """
        self.cursor=self.conn.cursor()
        return self.cursor
 
    def exec_sql(self,project,sql_type,sql):
        """
        执行sql语句
        :param sql_type:
        :param sql:
        :return:
        """
        self.sql_conf = self._get_sql_conf(project)
        try:
            if sql_type == 'select_one':
                self.connect_db()
                cursor = self.get_cursor()
                cursor.execute(sql)
                result = cursor.fetchone()
            elif sql_type == 'select_list':
                self.connect_db()
                cursor = self.get_cursor()
                cursor.execute(sql)
                result = cursor.fetchall()
            elif sql_type == 'update' or sql_type == 'del' or sql_type == 'insert':
                self.connect_db()
                result = self.get_cursor().execute(sql)
            self.conn.commit()
            self.cursor.close()
            self.conn.close()
            return result
        except Exception as e:
            self.log.error("执行sql语句报错:{0}".format(e))
 
 
if __name__ == '__main__':
    test = ExecSql()
    a=test.exec_sql('lxk',"select_list","sql)
    print(aaa)

九、日志方法

Python日志处理logging模块详解 请参考这篇文章。

代码语言:javascript
复制
#!/usr/bin/env python
# _*_coding:utf-8 _*_
import os, time, logging
log_path=os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'logs')
if not os.path.exists(log_path): os.mkdir(log_path)
 
 
class Log(object):
    """
    log日志类
    """
 
    def __init__(self):
        self.logname = os.path.join(log_path, '%s.log' % time.strftime('%Y_%m_%d'))
        self.logger = logging.getLogger()
        self.logger.setLevel(logging.DEBUG)
        self.formatter = logging.Formatter('[%(asctime)s]-%(filename)s]-%(levelname)s:%(message)s')
 
    def __console(self, level, message):
        fh=logging.FileHandler(self.logname, 'a', 'utf-8')
        fh.setLevel(logging.DEBUG)
        fh.setFormatter(self.formatter)
        self.logger.addHandler(fh)
        ch = logging.StreamHandler()
        ch.setLevel(logging.INFO)
        ch.setFormatter(self.formatter)
        self.logger.addHandler(ch)
        if level == 'info':
            self.logger.info(message)
        elif level == 'debug':
            self.logger.debug(message)
        elif level == 'warning':
            self.logger.warning(message)
        elif level == 'error':
            self.logger.error(message)
        self.logger.removeHandler(ch)
        self.logger.removeHandler(fh)
        fh.close()
 
    def debug(self, message):
        self.__console('debug', message)
 
    def info(self, message):
        self.__console('info', message)
 
    def warning(self, message):
        self.__console('warning', message)
 
    def error(self, message):
        self.__console('error', message)
 
 
if __name__ == '__main__':
    log = Log()
    log.info("---测试---")

十、断言方法

代码语言:javascript
复制
#!/usr/bin/env python
# _*_coding:utf-8_*_
from common.execSql import ExecSql
from common.logs import Log
 
class Assertion(object):
    log=Log()
    sql_values_list = []
    response_values = []
 
    def __init__(self):
        self.test=ExecSql().exec_sql
 
    def get_sql_data(self,project,sql_type,sql):
        '''
        查询sql数据组合list
        :param project:
        :param sql_type:
        :param sql:
        :return:
        '''
        try:
            sql_values=self.test(project,sql_type,sql)
            for i in sql_values:
                for j in i:
                    self.sql_values_list.append(j)
        except Exception as e:
            self.log.error("查询sql数据组合list报错{}".format(e))
 
 
    def get_response_data(self,response_data, keys=[]):
        '''
        获取接口响应数据组合list
        :param response_data:
        :param keys:
        :return:
        '''
        try:
            if isinstance(response_data, list):
                for value in response_data:
                    if isinstance(value, list) or isinstance(value, dict):
                        self.get_response_data(value, keys)
            elif isinstance(response_data, dict):
                for i, j in sorted(response_data.items()):
                    if i in keys:
                        self.response_values.append(j)
                    else:
                        self.get_response_data(j, keys)
            else:
                pass
        except Exception as e:
            self.log.error("获取接口响应数据组合list报错{}".format(e))
 
    def asser(self,function,casename,expect,response_data,assert_type=None):
        '''
        断言
        :param assert_type:
        :param expect:
        :param response_data:
        :return:
        '''
        try:
            if assert_type=='type1':
                assert self.sql_values_list==self.response_values
                self.log.info("查询sql数据组合list为{}".format(self.sql_values_list))
                self.log.info("接口响应数据组合list为{}".format(self.response_values))
            assert eval(expect)['code'] == response_data['code']
            assert eval(expect)['msg'] == response_data['msg']
            self.log.info("{}——{}【PASS】".format(function,casename))
        except Exception as e:
            self.log.error("{}——{}【PASS】{}".format(function,casename,e))
 
if __name__ == '__main__':
    # sql="sql"
    # test=Assertion()
    # test.get_sql_data(self,project,sql_type,sql)
    self.log.error("查询sql数据组合list报错{}".format(e))

十一、conftest登录获取token

conftest相关文章请参考:

Pytest全局用例共用之conftest.py详解

Python pytest框架之@pytest.fixture()和conftest详解

代码语言:javascript
复制
#!/usr/bin/env python
# _*_coding:utf-8_*_
import pytest,os,yaml,requests
from common.readFile import ReadFile
from common.login import Login
 
yaml_data=ReadFile().read_yaml('yaml_path')
 
@pytest.fixture(scope='session')
def get_lxk_c_headers():
    """
    登录获取token更新headers
    :return:
    """
    headers=yaml_data['lxk']['headers']
    token=Login().lxk_c_login('lxk_c',yaml_data['lxk']['account']['c_account'])['data']['token']
    headers['Authorization']=token
    return headers

十二、测试用例方法

测试用例执行相关请参考:

Pytest标记用例失败之xfail

Pytest跳过执行之@pytest.mark.skip()详解

Pytest自定义标记mark及指定文件/类/方法/用例执行

Pytest fixture之request传参

Pytest@pytest.mark.parametrize一键生成接口正交试验用例

Pytest脚本中用例运行方式

【Pytest篇】装饰器@pytest.mark.parametrize多样参数化(二)

Pytest装饰器@pytest.mark.parametrize数据驱动(三)

Python Pytest装饰器@pytest.mark.parametrize详解

Python Pytest中fixture之yield唤醒teardown和终结函数addfinalizer

Pytest之@pytest.mark.usefixtures()、叠加usefixtures、(autouse=True)详解

Python Pytest前置setup和后置teardown详解

代码语言:javascript
复制
#!/usr/bin/env python
# _*_coding:utf-8_*_
 
import pytest
from common.readFile import ReadFile
from common.request import RunMethod
from common.assertion import Assertion
from common.execSql import ExecSql
from common.prefixSqlData import MakeSqlData
import allure
 
data = ReadFile().read_excel('lxk', '我的报名')
 
@pytest.mark.parametrize('function,casename,type,run,url,hearders,param,sql1,sql2,sql3,asserttype,expect1,expect2,expect3',data)
class Test(object):
    '''我的报名'''
 
    request = RunMethod().run_main
    assertion = Assertion()
    exec_sql = ExecSql().exec_sql
    yaml_data = ReadFile().read_yaml('yaml_path')['lxk']
    sql_yaml_data = ReadFile().read_yaml('sql_yaml_path')['lxk']
    prefix_sql_data = MakeSqlData('lxk').make_sql_data
 
 
    def setup_class(self):
        '''
        数据初始化
        :return:
        '''
        prefix_data=self.prefix_sql_data(self.yaml_data['account']['b_account'], self.yaml_data['account']['c_account'])
        company_id = self.exec_sql('lxk', self.sql_yaml_data['查企业id'][0], self.sql_yaml_data['查企业id'][1].format(self.yaml_data['account']['b_account']))[0][0]
        task_id = self.exec_sql('lxk', self.sql_yaml_data['查任务id'][0], self.sql_yaml_data['查任务id'][1].format(self.yaml_data['account']['b_account']))[0][0]
        self.exec_sql('lxk', self.sql_yaml_data['报名任务'][0], self.sql_yaml_data['报名任务'][1].format(prefix_data['user_id'], task_id, company_id,self.yaml_data['account']['c_account'],1))
        self.exec_sql('lxk', self.sql_yaml_data['报名任务'][0], self.sql_yaml_data['报名任务'][1].format(prefix_data['user_id'], task_id, company_id,self.yaml_data['account']['c_account'],2))
        self.exec_sql('lxk', self.sql_yaml_data['报名任务'][0], self.sql_yaml_data['报名任务'][1].format(prefix_data['user_id'], task_id, company_id,self.yaml_data['account']['c_account'],3))
        self.exec_sql('lxk', self.sql_yaml_data['报名任务'][0], self.sql_yaml_data['报名任务'][1].format(prefix_data['user_id'], task_id, company_id,self.yaml_data['account']['c_account'],4))
        self.exec_sql('lxk', self.sql_yaml_data['报名任务'][0], self.sql_yaml_data['报名任务'][1].format(prefix_data['user_id'], task_id, company_id,self.yaml_data['account']['c_account'],5))
        self.exec_sql('lxk', self.sql_yaml_data['报名任务'][0], self.sql_yaml_data['报名任务'][1].format(prefix_data['user_id'], task_id, company_id,self.yaml_data['account']['c_account'],7))
        self.exec_sql('lxk', self.sql_yaml_data['报名任务'][0], self.sql_yaml_data['报名任务'][1].format(prefix_data['user_id'], task_id, company_id,self.yaml_data['account']['c_account'], 8))
 
    def teardown_class(self):
        '''
        数据清理
        :return:
        '''
        self.exec_sql('lxk', self.sql_yaml_data['删除已报名任务'][0], self.sql_yaml_data['删除已报名任务'][1].format(self.yaml_data['account']['c_account']))
        self.exec_sql('lxk', self.sql_yaml_data['解除用户团队'][0], self.sql_yaml_data['解除用户团队'][1].format(self.yaml_data['account']['c_account']))
 
    #@allure.feature('蓝薪卡')
    @allure.story('lxk_我的报名')
    def test_apply_task(self,get_lxk_c_headers,function,casename,type,run,url,hearders,param,sql1,sql2,sql3,asserttype,expect1,expect2,expect3):
        '''
        我的报名
        :param get_lxk_c_headers:
        :param function:
        :param casename:
        :param type:
        :param run:
        :param url:
        :param hearders:
        :param param:
        :param sql1:
        :param sql2:
        :param sql3:
        :param asserttype:
        :param expect1:
        :param expect2:
        :param expect3:
        :return:
        '''
        response_data = self.request(type,self.yaml_data['url']+url,get_lxk_c_headers,eval(param))
        self.assertion.get_sql_data('lxk',eval(sql1)[0],eval(sql1)[1].format(self.yaml_data['account']['c_account']))
        self.assertion.get_response_data(response_data,eval(expect2))
        self.assertion.asser(function,casename,expect1,response_data,asserttype)
 
 
 
if __name__ == "__main__":
    pytest.main(["-s", "test_001_applyTask.py"])

十三、run_all_case主程序执行入口

代码语言:javascript
复制
#!/usr/bin/env python
# _*_coding:utf-8_*_
import pytest,os,allure
if __name__ == "__main__":
 
    pytest.main(['-s',''])
    #生成测试报告json
    pytest.main(["-s", "-q", '--alluredir', 'C:/Users/wangli/PycharmProjects/PytestAutomation/report/result'])
    #将测试报告转为html格式
    split='allure '+'generate '+'C:/Users/wangli/PycharmProjects/PytestAutomation/report/result '+'-o '+'C:/Users/wangli/PycharmProjects/PytestAutomation/report/html '+'--clean'
    os.system('cd C:/Users/wangli/PycharmProjects/PytestAutomation/report')
    os.system(split)
    print(split)

十四、测试报告如下

测试报告相关文章,请参考:

【Pytest篇】Allure生成漂亮的HTML图形化测试报告(一))

Pytest框架集成Allure定制测试报告详解(一)

未完,待续————如果觉得看了收获到了,请关注+帮忙转发,感谢!!!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-01-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 橙子探索测试 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 SQL Server
腾讯云数据库 SQL Server (TencentDB for SQL Server)是业界最常用的商用数据库之一,对基于 Windows 架构的应用程序具有完美的支持。TencentDB for SQL Server 拥有微软正版授权,可持续为用户提供最新的功能,避免未授权使用软件的风险。具有即开即用、稳定可靠、安全运行、弹性扩缩等特点。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档