云效跨账号迁移,你学会了吗?

开发 前端
为了确保业务连续性和数据完整性,针对源阿里云账号即将停用的情况,我们完成了云效(DevOps平台)的数据迁移工作。由于云效无法直接通过实例迁移至目标账号,我们采取了数据导出与导入的方式,确保所有项目、流水线配置及代码仓库等数据完整无误地迁移到目标账号下。至此,平稳顺利的完成此次迁移。

背 景  

由于业务调整或组织架构变更,原阿里云账号将不再继续使用。为了确保业务连续性和数据完整性,需要将源账号下的所有云产品及其相关数据完整、安全地迁移到目标账号。由于云效(DevOps平台)无法直接通过实例划拨至目标账号,因此需要对云效中的数据进行迁移。

创建企业

在目标账号上点击

https://devops.console.aliyun.com/organizations/standard 

此链接,进入云效控制台界面,点击“新建组织”按钮。

图片图片

选择“云效DevOps”点击“立即开启”按钮。

图片图片

根据源账号云效,填写“组织名称”和“研发组织规模”,填写完成后点击“立即创建”按钮,完成云效企业的创建。

图片图片

新建项目

进入刚新建企业的控制台后,点击“项目协作 Projects”旁的“进入工作”按钮。

图片图片

点击“新建”按钮,创建项目。

图片图片

根据源账号项目类型,选择对应的项目模板。

图片图片

根据源账号项目信息进行填写,填写完成后点击“新建”按钮。

图片图片

新建完成后的项目如下图所示:

图片图片

迁移需求

源账号导出需求

在源账号需求界面,点击“批量操作”,在下拉菜单中单击“导出全部”。

图片图片

全选所有要导出的属性,完成后点击“开始导出”按钮。

图片图片

在跳转后的“批量操作记录”界面,等待“进展”的进度条显示成功后,点击右边的“下载”图表按钮,下载数据文件。

图片图片

需求数据表格打开后如下图所示:

图片图片

目标账号导入需求

在目标账号的任务界面右上角,点击“导入数据”按钮。

图片图片

选中“包含子工作项”后,点击“下载模板”按钮。

图片图片

将源账号导出的数据,根据导入模板的格式,写入到该excel表格里。

图片图片

注意:负责人必须为当前企业已存在的用户名称,迭代为当前项目已创建的迭代,填写不存在的信息会导致导入报错

返回到导入数据界面中,继续点击“下一步”按钮。

图片图片

将刚填写完数据的导入模板上传上去,完成后点击“开始导入”按钮。

图片图片

直到“导入工作项”显示为成功,则表示需求数据导入完成。

图片图片

检查需求数据是否跟源账号上的数据一致。

图片图片

迁移任务

源账号导出任务

在源账号任务界面,点击“批量操作”,在下拉菜单中单击“导出全部”。

图片图片

全选所有要导出的属性,完成后点击“开始导出”按钮。

图片图片

查看导出的任务数据,如下图所示:

图片图片

目标账号导入任务

在目标账号的任务界面右上角,点击“导入数据”按钮。

图片图片

选中“包含子工作项”后,点击“下载模板”按钮。

图片图片

将源账号导出的数据,根据导入模板的格式,写入到该excel表格里。

图片图片

和迁移需求一样,点击“下一步”按钮,将任务数据导入到目标账号中。

图片图片

迁移代码组、代码仓库、迭代

代码和迭代无法像需求和任务一样批量导出导入,为了提高效率和准确性,我们采用代码化方式批量迁移代码组、代码仓库和迭代,避免手动操作的费时费力及潜在错误。

获取组织ID

代码里需要用到“组织ID”,点击头像下拉菜单里的“管理后台”按钮,在“基本信息”页面即可看到“组织ID”。

图片图片

获取企业空间ID

代码里需要用到“企业空间ID”,点击

https://next.api.aliyun.com/api/devops/2021-06-25/GetCodeupOrganizatio

此链接,在“identity 企业标识”输入框内输入“组织ID”,完成后点击“发起调用”按钮。在左侧“调用结果”页面可以看到“namespaceId”,即为企业空间ID。

图片图片

获取个人访问令牌

点击头像,在下拉菜单中点击“个人设置”,进入个人设置界面,选择“个人访问令牌”,点击“创建访问令牌”按钮。

图片

输入“令牌名称”,选择到期时间,按需选择权限。代码管理权限必选,后续要用它进行代码仓库数据导入操作。完成上述配置后,点击“新建”按钮创建个人访问令牌。

图片

执行代码批量迁移

运行“源账号导出代码”,会print输出代码组、代码仓库和迭代的信息,运行“目标账号导入代码”时按代码提示输入代码组、代码仓库和迭代的信息,即可完成导入。执行前请在测试环境运行测试!!!

源账号导出代码

# -*- coding: utf-8 -*-
# This file is auto-generated, don't edit it. Thanks.
import sys,time
from typing import List
from alibabacloud_devops20210625.client import Client as devops20210625Client
from alibabacloud_tea_openapi import models as open_api_models
from alibabacloud_devops20210625 import models as devops_20210625_models
from alibabacloud_tea_util import models as util_models
from alibabacloud_tea_util.client import Client as UtilClient
ak="源账号AK"
sk="源账号SK"
organization_id='源账号组织ID'
parent_id= 源账号企业空间ID
class Sample:
    def __init__(self):
        pass
    @staticmethod
    def create_client(
        access_key_id: str,
        access_key_secret: str,
    ) -> devops20210625Client:
        config = open_api_models.Config(access_key_id=ak,access_key_secret=sk)
        config.endpoint = f'devops.cn-hangzhou.aliyuncs.com'
        return devops20210625Client(config)
    #导出代码组
    @staticmethod
    def ListGroupMember(
            args: List[str],
    ) -> None:
        organization_id = args[0]
        groupId = args[1]
        User_list=[]
        client = Sample.create_client(ak, sk)
        list_group_member_request = devops_20210625_models.ListGroupMemberRequest(organization_id=organization_id)
        runtime = util_models.RuntimeOptions()
        headers = {}
        Users = client.list_group_member_with_options(groupId, list_group_member_request, headers,runtime).body.result
        for User in Users:
            User_dic={}
            User_dic["name"]=User.name
            User_dic["access_level"] = User.access_level
            User_list.append(User_dic)
        return User_list
    @staticmethod
    def ListRepositoryGroups(
        args: List[str],
    ) -> None:
        organization_id=args[0]
        parent_id=args[1]
        ListRepositoryGroups_list=[]
        client = Sample.create_client(ak, sk)
        list_repository_groups_request = devops_20210625_models.ListRepositoryGroupsRequest(organization_id=organization_id,parent_id=parent_id,page_size=100)
        runtime = util_models.RuntimeOptions()
        headers = {}
        ListRepositoryGroups=client.list_repository_groups_with_options(list_repository_groups_request, headers, runtime).body.result
        for RepositoryGroups in ListRepositoryGroups:
            ListRepositoryGroup_dic = {}
            ListRepositoryGroup_dic["id"] = RepositoryGroups.id
            ListRepositoryGroup_dic["name"] =RepositoryGroups.name
            ListRepositoryGroup_dic["description"] =RepositoryGroups.description
            ListRepositoryGroup_dic["path"] = RepositoryGroups.path
            ListRepositoryGroup_dic["visibility_level"] = RepositoryGroups.visibility_level
            User_list=Sample.ListGroupMember([organization_id, str(RepositoryGroups.id)])
            ListRepositoryGroup_dic["user_list"] = User_list
            ListRepositoryGroups_list.append(ListRepositoryGroup_dic)
        return ListRepositoryGroups_list
    #获取代码仓库信息
    @staticmethod
    def GetRepositorymain(
            args: List[str],
    ) -> None:
        organization_id = args[0]
        identity = args[1]
        client = Sample.create_client(ak, sk)
        get_repository_request = devops_20210625_models.GetRepositoryRequest(organization_id=organization_id,identity=identity)
        runtime = util_models.RuntimeOptions()
        headers = {}
        http_url_to_repository = client.get_repository_with_options(get_repository_request, headers,runtime).body.repository.http_url_to_repository
        return http_url_to_repository
    @staticmethod
    def ListRepositoriesmain(
            args: List[str],
    ) -> None:
        organization_id = args[0]
        client = Sample.create_client(ak, sk)
        list_repositories_request = devops_20210625_models.ListRepositoriesRequest(organization_id=organization_id,per_page=100)
        runtime = util_models.RuntimeOptions()
        headers = {}
        ListRepositories = client.list_repositories_with_options(list_repositories_request, headers,runtime).body.result
        repository_list = []
        for Repository in ListRepositories:
            repository_dic = {}
            repository_dic["name"] = Repository.name
            repository_dic["path"] = Repository.path
            repository_dic["namespace_id"] = Repository.namespace_id
            repository_dic["description"] = Repository.description
            repository_dic["visibility_level"] = Repository.visibility_level
            http_url_to_repository = Sample.GetRepositorymain([organization_id, Repository.id])
            repository_dic["import_url"] = http_url_to_repository
            repository_list.append(repository_dic)
        return repository_list
    # 导出迭代信息
    @staticmethod
    def ListProjectsmain(
            args: List[str],
    ) -> None:
        organization_id = args[0]
        client = Sample.create_client(ak, sk)
        list_projects_request = devops_20210625_models.ListProjectsRequest(category='Project')
        runtime = util_models.RuntimeOptions()
        headers = {}
        projects = client.list_projects_with_options(organization_id, list_projects_request, headers,runtime).body.projects
        projects_info = {}
        for project in projects:
            identifier = project.identifier
            name = project.name
            projects_info[name] = identifier
        return projects_info
    @staticmethod
    def ListSprintsmain(
            args: List[str],
    ) -> None:
        organization_id = args[0]
        space_identifier = args[1]
        client = Sample.create_client(ak, sk)
        list_sprints_request = devops_20210625_models.ListSprintsRequest(space_identifier=space_identifier,space_type='Project',max_results=200)
        runtime = util_models.RuntimeOptions()
        headers = {}
        sprints = client.list_sprints_with_options(organization_id, list_sprints_request, headers,runtime).body.sprints
        sprint_list = []
        for sprint in sprints:
            sprint_dic = {}
            sprint_dic["name"] = sprint.name
            sprint_dic["start_date"] = time.strftime('%Y-%m-%d', time.localtime(sprint.start_date / 1000))
            sprint_dic["end_date"] = time.strftime('%Y-%m-%d', time.localtime(sprint.end_date / 1000))
            sprint_list.append(sprint_dic)
        return sprint_list
if __name__ == '__main__':
    #获取代码组信息
    ListRepositoryGroups_list=Sample.ListRepositoryGroups([organization_id,parent_id])
    print("代码组信息:"+str(ListRepositoryGroups_list))
    #获取代码仓库信息
    repository_list = Sample.ListRepositoriesmain([organization_id, ])
    print("代码仓库信息:"+str(repository_list))
    #获取迭代信息
    while True:
        projects_info = Sample.ListProjectsmain([organization_id, ])
        print("项目名称和项目ID的对应关系"+str(projects_info))
        identifier = input("请输入要导出的迭代是哪个项目的ID(不需要导出请输入no):")
        if identifier=="no":
            break
        sprint_list = Sample.ListSprintsmain([organization_id, identifier])
        print("迭代的信息:"+str(sprint_list))
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.

目标账号导入代码

# -*- coding: utf-8 -*-
# This file is auto-generated, don't edit it. Thanks.
from typing import List
from alibabacloud_devops20210625.client import Client as devops20210625Client
from alibabacloud_tea_openapi import models as open_api_models
from alibabacloud_devops20210625 import models as devops_20210625_models
from alibabacloud_tea_util import models as util_models
from alibabacloud_tea_util.client import Client as UtilClient
import ast
ak="目标账号AK"
sk="目标账号SK"
organization_id="目标账号组织ID"
namespaceId="目标账号企业空间ID"
import_account="目标账号个人访问令牌名称"
import_token="目标账号个人访问令牌token"
class Sample:
    def __init__(self):
        pass
    @staticmethod
    def create_client(
        access_key_id: str,
        access_key_secret: str,
    ) -> devops20210625Client:
        config = open_api_models.Config(access_key_id=ak,access_key_secret=sk)
        config.endpoint = f'devops.cn-hangzhou.aliyuncs.com'
        return devops20210625Client(config)
    #导入代码组
    @staticmethod
    def CreateRepositoryGroupmain(
        args: List[str],
    ) -> None:
        organization_id = args[0]
        name = args[1]
        path = args[2]
        visibility_level = args[3]
        namespaceId = args[4]
        description = args[5]
        client = Sample.create_client(ak,sk)
        create_repository_group_request = devops_20210625_models.CreateRepositoryGroupRequest(organization_id=organization_id,name=name,path=path,visibility_level=visibility_level,parent_id=namespaceId,descriptinotallow=description)
        runtime = util_models.RuntimeOptions()
        headers = {}
        print(client.create_repository_group_with_options(create_repository_group_request, headers, runtime))
    #获取源代码组ID与目标代码组ID的对应关系
    @staticmethod
    def ListRepositoryGroups(
            args: List[str],
    ) -> None:
        organization_id = args[0]
        parent_id = args[1]
        ListRepositoryGroups_list = []
        client = Sample.create_client(ak,sk)
        list_repository_groups_request = devops_20210625_models.ListRepositoryGroupsRequest(organization_id=organization_id,parent_id=parent_id,page_size=100)
        runtime = util_models.RuntimeOptions()
        headers = {}
        ListRepositoryGroups = client.list_repository_groups_with_options(list_repository_groups_request, headers,runtime).body.result
        for RepositoryGroups in ListRepositoryGroups:
            ListRepositoryGroup_dic = {}
            ListRepositoryGroup_dic["id"] = RepositoryGroups.id
            ListRepositoryGroup_dic["name"] = RepositoryGroups.name
            ListRepositoryGroup_dic["description"] = RepositoryGroups.description
            ListRepositoryGroup_dic["path"] = RepositoryGroups.path
            ListRepositoryGroup_dic["visibility_level"] = RepositoryGroups.visibility_level
            ListRepositoryGroups_list.append(ListRepositoryGroup_dic)
        return ListRepositoryGroups_list
    #导入代码仓库
    @staticmethod
    def CreateRepositorymain(
            args: List[str],
    ) -> None:
        organization_id = args[0]
        name = args[1]
        path = args[2]
        namespace_id = args[3]
        description = args[4]
        visibility_level = args[5]
        import_url = args[6]
        import_account = args[7]
        import_token = args[8]
        client = Sample.create_client(ak,sk)
        create_repository_request = devops_20210625_models.CreateRepositoryRequest(organization_id=organization_id,name=name,path=path,namespace_id=namespace_id,descriptinotallow=description,visibility_level=visibility_level,import_url=import_url,import_account=import_account,import_token=import_token)
        runtime = util_models.RuntimeOptions()
        headers = {}
        print(client.create_repository_with_options(create_repository_request, headers, runtime))
    # 得到项目名称和项目ID的对应关系
    @staticmethod
    def ListProjectsmain(
            args: List[str],
    ) -> None:
        organization_id = args[0]
        client = Sample.create_client(ak,sk)
        list_projects_request = devops_20210625_models.ListProjectsRequest(category='Project')
        runtime = util_models.RuntimeOptions()
        headers = {}
        projects = client.list_projects_with_options(organization_id, list_projects_request, headers,runtime).body.projects
        projects_info = {}
        for project in projects:
            identifier = project.identifier
            name = project.name
            projects_info[name] = identifier
        return projects_info
    #创建迭代
    @staticmethod
    def CreateSprintmain(
        args: List[str],
    ) -> None:
        organization_id=args[0]
        identifier=args[1]
        FROM_diedai=args[2]
        staff_id=args[3]
        client = Sample.create_client(ak,sk)
        create_sprint_request = devops_20210625_models.CreateSprintRequest(
            staff_ids=[staff_id],  #ram用户uid
            name=FROM_diedai["name"],  #迭代名称
            space_identifier=identifier,  #项目ID
            start_date=FROM_diedai["start_date"],  #迭代开始时间
            end_date=FROM_diedai["end_date"]     #迭代结束时间
        )
        runtime = util_models.RuntimeOptions()
        headers = {}
        print(client.create_sprint_with_options(organization_id, create_sprint_request, headers, runtime))
if __name__ == '__main__':
    # 导入代码组
    FROMListRepositoryGroups_list=ast.literal_eval(input("请输入源账号导出的代码组列表:"))
    for RepositoryGroup in FROMListRepositoryGroups_list:
        Sample.CreateRepositoryGroupmain([organization_id,RepositoryGroup["name"],RepositoryGroup["path"],RepositoryGroup["visibility_level"],namespaceId,RepositoryGroup["description"]])
    # 获取源代码组ID与目标代码组ID的对应关系
    TOListRepositoryGroups_list = Sample.ListRepositoryGroups([organization_id, namespaceId])
    FROM_TOListRepositoryGroupID_dic = {}
    for FROMListRepositoryGroup in FROMListRepositoryGroups_list:
        for TOListRepositoryGroup in TOListRepositoryGroups_list:
            if FROMListRepositoryGroup["name"] == TOListRepositoryGroup["name"]:
                FROM_TOListRepositoryGroupID_dic[FROMListRepositoryGroup["id"]] = TOListRepositoryGroup["id"]
    FROMparent_id=ast.literal_eval(input("请输入源账号的parent_id,也就是namespace_id:"))
    FROM_TOListRepositoryGroupID_dic[FROMparent_id]=int(namespaceId)
    print("源账号代码组ID与目标账号代码组ID的对应关系:"+str(FROM_TOListRepositoryGroupID_dic))
    #导入代码仓库
    FROMrepository_list=ast.literal_eval(input("请输入源账号导出的代码仓库列表:"))
    for FROMrepository in FROMrepository_list:
        Sample.CreateRepositorymain([organization_id, FROMrepository["name"], FROMrepository["path"],FROM_TOListRepositoryGroupID_dic[FROMrepository["namespace_id"]], FROMrepository["description"],FROMrepository["visibility_level"], FROMrepository["import_url"], import_account, import_token])
    # 导入迭代
    while True:
        projects_info = Sample.ListProjectsmain([organization_id, ])
        print("项目名称和项目ID的对应关系:"+str(projects_info))
        identifier = input("请输入要导入的迭代是哪个项目的ID(不需要导出请输入no):")
        if identifier=="no":
            break
        print("用户ID:{用户A:用户A的RAM账号UID,用户B:用户B的RAM账号UID,用户C:用户C的RAM账号UID}")
        print("项目与负责人的对应关系:{'项目A':'负责人A','项目B':'负责人B','项目C':'负责人C'}")
        staff_id = input("请输入要导入迭代负责人的RAM账号UID:")
        FROM_diedai_list = ast.literal_eval(input("请输入要导入的迭代内容列表:"))
        for FROM_diedai in FROM_diedai_list:
            Sample.CreateSprintmain([organization_id, identifier, FROM_diedai, staff_id])
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.

总 结

为了确保业务连续性和数据完整性,针对源阿里云账号即将停用的情况,我们完成了云效(DevOps平台)的数据迁移工作。由于云效无法直接通过实例迁移至目标账号,我们采取了数据导出与导入的方式,确保所有项目、流水线配置及代码仓库等数据完整无误地迁移到目标账号下。至此,平稳顺利的完成此次迁移。

责任编辑:武晓燕 来源: 新钛云服
相关推荐

2021-11-03 09:51:45

鸿蒙HarmonyOS应用

2011-03-07 17:11:21

云迁移云转型

2015-10-19 09:42:06

小鸟云云服务器

2018-09-07 10:23:46

云备份混合云存储

2023-10-19 16:39:38

2021-06-16 15:18:03

鸿蒙HarmonyOS应用

2021-06-02 10:50:35

腾讯云销售易云迁移

2019-03-01 17:55:59

企业云计算云平台

2021-12-08 09:23:39

云迁移云开发云计算

2023-02-07 15:33:16

云迁移数据中心云计算

2010-03-09 09:49:01

Oracle跨平台迁移

2009-03-23 09:05:01

2012-04-01 09:35:07

云计算云标准

2013-03-21 10:32:59

视讯云阿里云云平台

2013-11-27 09:57:53

云应用迁移云应用安全云计算迁移规划

2021-04-27 15:20:41

人工智能机器学习技术

2010-11-18 23:24:42

云迁移

2016-10-24 10:01:03

云计算

2019-11-27 10:55:36

云迁移云计算云平台

2015-10-10 09:50:26

AWS云迁移云迁移工具
点赞
收藏

51CTO技术栈公众号