前后端分离项目(springboot+vue+mybatis)-阿里云上部署数据库与区块链-3

文章目录

参考文献

什么是区块链
https://blog.csdn.net/HUIBUR/article/details/137952996
国产区块链webase平台
https://fisco-bcos-documentation.readthedocs. tyio/zh-cn/latest/docs/introduction.html
https://blog.csdn.net/gitblog_00018/article/details/138061832

1.环境配置

1.1. 本地环境

springboot本地部署:
操作系统:Windows 11 x64
处理器:11th Gen Intel® Core™ i5-11300H @ 3.10GHz 3.11 GHz
区块链本地部署:
操作系统:ubuntu 20
处理器:11th Gen Intel® Core™ i5-11300H @ 3.10GHz 3.11 GHz
项目环境配置:
jdk 19
apache maven 3.9.3
apache tomcat 9.0.7
mysql 5.7.26
navicat 16
node 16.17.1
npm 8.15.0

1.2.阿里云环境

2.区块链

2.1 前言

       ~~~~~~        在当今数字化时代,区块链技术已经成为全球范围内备受瞩目的话题。从金融到供应链,从物联网到数字身份,区块链正在以惊人的速度渗透到各个行业,并在重塑着我们的社会和经济格局。
       ~~~~~~       区块链最初因其作为比特币背后技术的而引起人们的关注。但现在,它已经远远超出了仅仅作为数字货币的用途。它被认为是一种革命性的技术,有潜力改变我们传统的商业模式、加强数据安全和可信度,并促进更加公平和透明的交易。
       ~~~~~~       区块链的核心思想是建立一个去中心化、不可篡改的分布式账本,使得数据的传输和存储变得更加安全、高效和可靠。在这个账本上,所有的交易都被记录下来,并通过加密技术保护,使得数据不易被篡改或伪造。这种去中心化的特性意味着不再需要依赖于单一的中心化机构或权威来验证和管理数据,而是由网络中的参与者共同维护。
       ~~~~~~       正是由于这些特性,区块链技术正在引发各个行业的变革。在金融领域,它正在改变支付和结算方式,提高资产交易的效率和安全性;在供应链领域,它正在提高物流信息的透明度和可追溯性;在数字身份领域,它正在帮助解决身份验证和个人隐私保护的问题。
       ~~~~~~       然而,尽管区块链技术充满了潜力,但也面临着一些挑战,如可扩展性、性能问题、法律法规等。因此,我们需要深入了解区块链技术,探索其优势和局限性,并不断创新和完善,以实现

2.2 什么是区块链

区块链是一种分布式数据库技术,它以块的形式存储数据,并使用密码学方法保证数据的安全性和完整性。每个块包含一定数量的交易信息,并通过加密链接到前一个块,形成一个不断增长的链条。这种设计使得数据在网络中无法被篡改,因为任何尝试修改一个块的数据都会破坏整个链的连续性。通过去中心化的网络结构,区块链技术实现了对数据的分布式共享和管理,从而在不需要信任中介的情况下确保了数据的安全和可靠性。
在这里插入图片描述

2.2.1 区块(Block):

区块是区块链中的基本单位,用于存储数据。每个区块包含了一定数量的交易信息,如数字货币的转账记录、智能合约的执行结果等。区块还包含一个称为区块头的元数据,其中包括了对上一个区块的引用、时间戳、随机数以及用于验证区块内容的哈希值等信息。

2.2.2 链(Chain):

区块链是由一系列按照特定顺序连接而成的区块构成的链条。每个区块中都包含了指向前一个区块的哈希值,形成了一个不可篡改的数据链。这种连续的链接保证了区块链中的数据不可逆地被记录和保存。

2.2.3 分布式账本(Distributed Ledger):

区块链是一种分布式账本,数据的存储和管理分布在网络中的多个节点上,而不是集中存储在单一的中心化服务器上。每个节点都包含了完整的账本副本,并通过共识机制来保持账本的一致性。这种分布式的特性使得区块链更加安全和可信。

2.2.4 去中心化(Decentralization):

区块链的去中心化特性意味着不存在单一的管理者或控制者,所有的参与者共同维护和管理网络。数据的验证和交易的确认是通过网络中的节点之间的协作和共识达成的,而不是依赖于单一的中心化机构。去中心化使得区块链网络更加民主、透明和公平。

总的来说,区块链的基本概念包括区块、链、分布式账本和去中心化等重要部分,这些概念共同构成了区块链技术的核心特性和工作原理。

2.3 技术原理

区块链技术的原理涉及到数据结构、加密技术和共识机制等多个方面:
在这里插入图片描述

2.3.1 区块链的数据结构:

区块链采用链式数据结构,由一系列按照特定顺序连接的区块构成。每个区块包含了一定数量的交易数据和区块头信息。区块头包括了前一个区块的哈希值、时间戳、随机数以及用于验证区块内容的哈希值等信息。区块链中的每个区块都通过其前一个区块的哈希值链接在一起,形成了一个不可篡改的数据链。

2.3.2加密技术:

区块链使用了多种加密技术来保护数据的安全性和隐私性。其中最重要的是哈希函数和公钥加密。
哈希函数用于将任意长度的数据转换为固定长度的哈希值。在区块链中,每个区块都包含了前一个区块的哈希值,通过哈希值链的方式保证了数据的完整性和连续性。
公钥加密技术用于实现数字签名和加密通信。数字签名用于验证交易的发送者身份和交易的完整性,确保交易不被篡改。加密通信则用于保护数据在网络中的传输过程中的安全性。

2.3.3、共识机制:

区块链通过共识机制来确保网络中的节点达成一致,从而保持数据的一致性和完整性。常见的共识机制包括工作量证明(PoW)、权益证明(PoS)、权益抵押(DPoS)等。
工作量证明(PoW)是比特币和许多其他区块链项目中最常见的共识机制,它要求节点通过解决一定的数学难题来证明其对网络的贡献,从而获得生成新区块的权利。权益证明(PoS)则是另一种常见的共识机制,它根据节点持有的加密货币数量来决定其对网络的投票权。

总的来说,区块链的技术原理涉及到链式数据结构、加密技术和共识机制等多个方面,这些技术共同构成了区块链技术的核心特性和工作原理。

2.4 架构模型

一般说来,区块链系统由数据层、网络层、共识层、激励层、合约层和应用层组成。
在这里插入图片描述
在这里插入图片描述

2.4.1 数据层

封装了底层数据区块以及相关的数据加密和时间戳等基础数据和基本算法。

2.4.2网络层

则包括分布式组网机制、数据传播机制和数据验证机制等;共识层主要封装网络节点的各类共识算法。

2.4.3 激励层

将经济因素集成到区块链技术体系中来,主要包括经济激励的发行机制和分配机制等。

2.4.4 合约层

主要封装各类脚本、算法和智能合约,是区块链可编程特性的基础。

2.4.5 应用层

则封装了区块链的各种应用场景和案例。该模型中,基于时间戳的链式区块结构、分布式节点的共识机制、基于共识算力的经济激励和灵活可编程的智能合约是区块链技术最具代表性的创新点。

2.5 区块链为什么能够代替数据库

2.5.1. 去中心化

传统数据库: 传统数据库通常是集中式的,依赖于一个或多个中央服务器进行数据存储和管理。所有的数据请求和操作都通过中央服务器进行,这使得系统存在单点故障风险。一旦中央服务器出现问题,整个系统就可能瘫痪。

区块链: 区块链是一种分布式账本技术,每个参与节点都有一个完整的数据库副本。这种去中心化的设计消除了单点故障的风险,即使某个节点失效,系统仍然可以正常运行。这提高了系统的鲁棒性和可靠性,特别是在需要高可用性的场景中。

2.5.2. 不可篡改性

传统数据库: 在传统数据库中,数据可以被随意修改和删除,尽管可以通过日志和权限控制来防止篡改,但仍然无法完全避免内部或外部的恶意行为。

区块链: 区块链通过密码学技术和共识机制确保数据一旦写入就无法篡改。每个区块都包含前一个区块的哈希值,形成一个链条结构,这使得任何篡改行为都会被立即发现。区块链的这种不可篡改性特别适用于需要高度数据完整性的场景,如金融交易和医疗记录。

2.5.3. 透明性和可追溯性

传统数据库: 传统数据库的访问权限通常是受限的,数据的透明性较低,需要信任管理员的诚实和系统的完整性。数据的追溯性也需要依赖复杂的日志记录和审计机制。

区块链: 区块链上的所有交易都是公开透明的,任何人都可以查看所有历史记录。这种透明性大大减少了信息不对称,提高了系统的信任度。同时,区块链的链式结构使得所有数据都具有可追溯性,任何数据的来源和变动都可以被清晰地追踪到。这在供应链管理、食品安全等需要严格追溯的领域具有重要意义。

2.5.4. 去信任化

传统数据库: 传统数据库的操作依赖于中央机构或第三方的信任,这对于多方参与、彼此不完全信任的环境中是一个难点。

区块链: 区块链通过共识机制(如PoW、PoS等)来确保数据的真实性和一致性,无需依赖中央机构或第三方。每个参与节点都参与数据的验证和记录,确保了数据的公正性和可靠性。这种去信任化的特点非常适用于多方协作、需要高信任度的场景,如跨境支付和联合审计。

2.5.5. 安全性

传统数据库: 传统数据库可以通过加密技术保护数据,但仍然存在被攻破的风险,尤其是当数据库被集中存储时,一旦被攻破,所有数据都可能泄露。

区块链: 区块链通过高级加密技术和分布式存储增强了数据的安全性。数据在传输和存储过程中都经过加密,且分布在多个节点上,攻击者难以同时攻破所有节点。即使某个节点被攻破,其他节点的数据仍然是安全的。

2.5.6. 智能合约

传统数据库: 在传统数据库中,自动化流程需要复杂的编程和外部系统集成,实现难度高且维护成本大。

区块链: 区块链支持智能合约,这是运行在区块链上的自动执行代码,当满足预设条件时自动触发。智能合约可以实现自动化的交易和流程,减少人工干预,提高效率和准确性。智能合约特别适用于需要自动化执行和严格规则遵循的场景,如保险理赔和供应链自动化。

2.5.7. 数据一致性

传统数据库: 在分布式数据库系统中,数据一致性问题是一个复杂的挑战,需要借助分布式一致性协议(如Paxos、Raft等)来解决,且实现和维护难度较高。

区块链: 区块链通过共识算法(如PoW、PoS等)确保所有节点的数据一致性和同步性。每个节点都参与共识过程,保证了整个系统数据的一致性和完整性。这种机制使得区块链在多方协作、需要高度一致性的场景中更具优势。

2.6 区块链平台FISCO BCOS

区块链技术的应用:研究如何利用区块链技术实现投票数据的安全存储和不可篡改性。将投票信息以区块的形式存储在区块链上,确保数据的安全性和可靠性。研究如何利用区块链的去中心化特性防止数据被篡改或删除。同时,研究如何利用区块链的透明性和可追溯性验证投票数据的真实性和完整性,增加投票过程的公正性和可信度。

2.6.1 区块链平台

        ~~~~~~~        区块链是一种分布式数据库,由多个节点共同维护,每个节点保存着完整的数据库副本。本系统为了保证投票的公平性,投票数据的安全性,可追溯性,不可篡改性,使用开源的FISCO BCOS区块链平台。
        ~~~~~~~        FISCO BCOS是一个面向联盟链的区块链平台,FISCO BCOS采用基于账本的多链架构,FISCO BCOS还支持联盟链模式,允许多个组织共同参与链的管理和运维。FISCO BCOS支持多种共识算法,使用Solidity语言编写智能合约,智能合约在链上执行,实现业务逻辑的自动化和可信执行。智能合约可以定义数据结构、业务规则和交易操作,通过调用合约的函数来进行数据的读写和业务逻辑的执行。
        ~~~~~~~        FISCO BCOS提供了隐私保护机制,包括数据加密、合约隐私、交易隐私等。通过使用零知识证明、同态加密等技术,可以保护链上数据和交易的隐私性,确保敏感信息不被泄露。
        ~~~~~~~        FISCO BCOS链保证数据存储在链上的每个节点上,而不是集中存储在单一的中心服务器上。这种去中心化的存储方式确保了数据的可靠性和安全性,防止数据的单点故障和篡改。每个数据交易都会被记录在一个区块中,并通过共识算法达成一致。一旦数据被写入区块链,就无法被修改或删除,保证了数据的不可篡改性。对于投票数据在链上进一步加密,数据安全性更高,由于每个数据交易都被记录在区块链上,因此可以实现数据的完全追溯。任何人都可以查看和验证过去的交易记录,从而建立信任和透明度。这对投票场景非常有用。

2.6.2 区块链模块设计

系统使用开源的FISCO BCOS区块链平台保证投票数据的安全性,可追溯性,不可篡改性。区块链部署的linux虚拟机上,利用虚拟机的ip地址进行与区块链进行交互。浏览器使用HTTP协议向虚拟机IP地址发送请求。请求经过本地网络,到达虚拟机所在的物理机器。物理机器根据请求的IP地址和端口号找到对应的虚拟机实例。请求被发送给虚拟机内的区块链服务器。服务器处理请求,生成HTTP响应。响应经过虚拟机内部网络,返回给物理机器。物理机器收到响应后,将其发送回浏览器。图1展示该过程架构图。
在这里插入图片描述
图1 区块链架构图
FISCO BCOS链使用Solidity语言编写智能合约,智能合约在链上执行。智能合约数据储存结构为串类型,通过调用合约的函数来进行数据的储存和查询的功能。由于性能问题,在linux虚拟机部署三个节点进行数据备份,保障链的完整性、安全性和可靠性。

2.7 区块链可视化平台webase

2.7.1 项目简介

是微众银行(WeBank)金融科技团队开发的一个开源项目,它是一个基于区块链的前端访问控制与管理平台,旨在为区块链应用提供安全、高效、易用的底层服务。WeBASE 提供了包括节点管理和监控、用户管理、合约管理等功能,支持联盟链场景下的多种主流区块链框架,如 Fabric 和 FISCO BCOS。

2.7.2技术分析

多链兼容:WeBASE 支持对接多种区块链平台,如 Hyperledger Fabric 和微众银行的 FISCO BCOS,这使得它能够灵活适应不同场景和需求。

安全性:提供证书管理,确保用户访问区块链的安全性;同时具备节点监控功能,可以实时监测并预警区块链网络中的异常情况。

易用性:通过 Web 界面提供友好的操作体验,无需深入了解区块链底层技术,即可进行节点管理、智能合约部署及调用等操作。

可扩展性:WeBASE 设计了模块化架构,方便添加新的功能或适配更多的区块链系统。

API 接口:提供了丰富的 API 接口,方便开发者集成到自己的业务系统中。

2.7.3 应用场景

企业级区块链应用:在金融、供应链、物联网等领域,企业可以利用 WeBASE 快速搭建对区块链网络的管理和访问。

研发与测试:开发人员可以在开发和测试环境中使用 WeBASE 进行区块链节点管理,提高效率。

教育与学习:对于学习区块链技术的人来说,WeBASE 是一个很好的实践工具,它可以简化操作,让初学者更专注于理解区块链的核心概念。

2.7.4 特点总结

多链支持:适应多种区块链框架。
安全可靠:内置证书管理,实时节点监控。
界面友好:Web 管理界面,降低使用门槛。
易于集成:丰富的 API 接口,便于业务系统集成。
模块化设计:易于扩展,满足未来需求。

2.8 区块链的实现

2.8.1平台搭建

在VMware软件上安装linux虚拟机,在虚拟机上先安装FISCO BCOS链所需有的所有前提环境,如mysql5.7、python3.8、jdk1.8等。利用FISCO BCOS官方文档一键部署webase平台。先下载所需安装包并更改common.properties文件相关配置,运行python3 deploy.py installAll命令成功安装,python3 deploy.py installAll一键启动,如图2所示。
在这里插入图片描述
图2 FISCO BCOS链启动图
Linux虚拟机上启动成功后,访问http://192.168.198.132:5000/#/home地址计入管理平台,如图3所示。
在这里插入图片描述
图3 FISCO BCOS管理平台图
FISCO BCOS管理平台,即WeBASE管理平台,是由四个WeBASE子系统组成的一套管理FISCO-BCOS联盟链的工具集。部署架构如图4所示。
在这里插入图片描述
图4 WeBASE四个服务的部署架构图

2.8.2 智能合约的编写

FISCO BCOS区块链向外部提供了接口,可以通过FISCO BCOS提供的SDK来调用这些接口。本系统基于Python SDK与区块链交互数据,具体可以安装FISCO BCOS官方文档进行部署。由于系统是将共识的数据上传到区块链上,利用区块链特性使得投票系统更加安全和公平。需要创建私钥用户和智能合约,私钥用户可以通过命令python console2.py newaccoun创建需要手动保存私钥,将智能合约通过私钥用户部署到链上。智能合约部署如图5所示。
在这里插入图片描述
在这里插入图片描述
图5 合约部署图
在合约部署成功后,可以在合约IDE页面的右上角点击发交易,向合约发送交易进行合约调用。合约调用如图6所示,其中,图6-1表示调用合约中的addCandidate方法来添加数据,图6-2表示调用合约中的getCandidateInfo方法来根据主键查询完整信息。
在这里插入图片描述
图6-1 合约调用图1
在这里插入图片描述
图6-2 合约调用图2

2.8.3 查看交易回执

交易发送成功后,将返回交易回执。可以在数据概览-交易列表-更多中根据transactionHash搜索交易,通过交易解析和Event解析查看可视化的交易回执信息。交易回执如图7所示。
在这里插入图片描述
图7-1 交易回执图1
在这里插入图片描述
图7-2 交易回执图2

3.数据库

3.1数据库的基本原理

3.1.1.什么是数据库

       ~~~~~~       你正在读本 SQL 教程,这表明你需要以某种方式与数据库打交道。SQL 正是用来实现这一任务的语言,因此在学习 SQL 之前,你应该对数据库及数据库技术的某些基本概念有所了解。
       ~~~~~~       你可能还没有意识到,其实自己一直在使用数据库。每当你在手机上选取联系人,或从电子邮件地址簿里查找名字时,就是在使用数据库。你在网站上进行搜索,也是在使用数据库。你在工作中登录网络,也需要依靠数据库验证用户名和密码。即使是在自动取款机上使用 ATM 卡,也要利用数据库进行密码验证和余额查询。
       ~~~~~~       虽然我们一直都在使用数据库,但对究竟什么是数据库并不十分清楚。更何况人们可能会使用同一个数据库术语表示不同的事物,进一步加剧了这种混乱。因此,我们首先给出一些最重要的数据库术语,并加以说明。

3.1.2 数据库

       ~~~~~~       每个人家里都会有冰箱,冰箱是用来干什么的?冰箱是用来存放食物的地方。
       ~~~~~~       同样的,数据库是存放数据的地方。正是因为有了数据库后,我们可以直接查找数据。例如你每天使用余额宝查看自己的账户收益,就是从数据库读取数据后给你的。

数据库(database)
保存有组织的数据的容器(通常是一个文件或一组文件)。

注意:误用导致混淆
人们通常用数据库这个术语来代表他们使用的数据库软件,这是不正确的,也因此产生了许多混淆。确切地说,数据库软件应称为数据库管理系统(DBMS)。数据库是通过 DBMS 创建和操纵的容器,而具体它究竟是什么,形式如何,各种数据库都不一样。

3.1.3表

       ~~~~~~       你往文件柜里放资料时,并不是随便将它们扔进某个抽屉就完事了的,而是在文件柜中创建文件,然后将相关的资料放入特定的文件中。
       ~~~~~~       在数据库领域中,这种文件称为表。表是一种结构化的文件,可用来存储某种特定类型的数据。表可以保存顾客清单、产品目录,或者其他信息清单。

表(table)
       ~~~~~~       某种特定类型数据的结构化清单。
       ~~~~~~       这里的关键一点在于,存储在表中的数据是同一种类型的数据或清单。决不应该将顾客的清单与订单的清单存储在同一个数据库表中,否则以后的检索和访问会很困难。应该创建两个表,每个清单一个表。
       ~~~~~~       数据库中的每个表都有一个名字来标识自己。这个名字是唯一的,即数据库中没有其他表具有相同的名字。

说明:表名
       ~~~~~~       使表名成为唯一的,实际上是数据库名和表名等的组合。有的数据库还使用数据库拥有者的名字作为唯一名的一部分。也就是说,虽然在一个数据库中不能两次使用相同的表名,但在不同的数据库中完全可以使用相同的表名。
       ~~~~~~       表具有一些特性,这些特性定义了数据在表中如何存储,包括存储什么样的数据,数据如何分解,各部分信息如何命名等信息。描述表的这组信息就是所谓的模式(schema),模式可以用来描述数据库中特定的表,也可以用来描述整个数据库(和其中表的关系)。

模式
关于数据库和表的布局及特性的信息。

3.1.4列和数据类型

表由列组成。列存储表中某部分的信息。

列(column)
       ~~~~~~       表中的一个字段。所有表都是由一个或多个列组成的。
       ~~~~~~       理解列的最好办法是将数据库表想象为一个网格,就像个电子表格那样。网格中每一列存储着某种特定的信息。例如,在顾客表中,一列存储顾客编号,另一列存储顾客姓名,而地址、城市、州以及邮政编码全都存储在各自的列中。

提示:数据分解
       ~~~~~~       正确地将数据分解为多个列极为重要。例如,城市、州、邮政编码应该总是彼此独立的列。通过分解这些数据,才有可能利用特定的列对数据进行分类和过滤(如找出特定州或特定城市的所有顾客)。如果城市和州组合在一个列中,则按州进行分类或过滤就会很困难。
       ~~~~~~       你可以根据自己的具体需求来决定把数据分解到何种程度。例如,一般可以把门牌号和街道名一起存储在地址里。这没有问题,除非你哪天想用街道名来排序,这时,最好将门牌号和街道名分开。
       ~~~~~~       数据库中每个列都有相应的数据类型。数据类型(datatype)定义了列可以存储哪些数据种类。例如,如果列中存储的是数字(或许是订单中的物品数),则相应的数据类型应该为数值类型。如果列中存储的是日期、文本、注释、金额等,则应该规定好恰当的数据类型。

数据类型
       ~~~~~~       允许什么类型的数据。每个表列都有相应的数据类型,它限制(或允许)该列中存储的数据。
       ~~~~~~       数据类型限定了可存储在列中的数据种类(例如,防止在数值字段中录入字符值)。数据类型还帮助正确地分类数据,并在优化磁盘使用方面起重要的作用。因此,在创建表时必须特别关注所用的数据类型。

注意:数据类型兼容
数据类型及其名称是 SQL 不兼容的一个主要原因。虽然大多数基本数据类型得到了一致的支持,但许多高级的数据类型却没有。更糟的是,偶然会有相同的数据类型在不同的 DBMS 中具有不同的名称。对此用户毫无办法,重要的是在创建表结构时要记住这些差异。

3.1.5 行

表中的数据是按行存储的,所保存的每个记录存储在自己的行内。如果将表想象为网格,网格中垂直的列为表列,水平行为表行。
例如,顾客表可以每行存储一个顾客。表中的行编号为记录的编号。

行(row)
表中的一个记录。

说明:是记录还是行?
你可能听到用户在提到行时称其为数据库记录(record)。这两个术语多半是可以互通的,但从技术上说,行才是正确的术语。

3.1.6 主键

表中每一行都应该有一列(或几列)可以唯一标识自己。顾客表可以使用顾客编号,而订单表可以使用订单 ID。雇员表可以使用雇员 ID。书目表则可以使用国际标准书号 ISBN。

主键(primary key)

一列(或几列),其值能够唯一标识表中每一行。
唯一标识表中每行的这个列(或这几列)称为主键。主键用来表示一个特定的行。没有主键,更新或删除表中特定行就极为困难,因为你不能保证操作只涉及相关的行,没有伤及无辜。

提示:应该总是定义主键
虽然并不总是需要主键,但多数数据库设计者都会保证他们创建的每个表具有一个主键,以便于以后的数据操作和管理。

表中的任何列都可以作为主键,只要它满足以下条件:
任意两行都不具有相同的主键值;
每一行都必须具有一个主键值(主键列不允许空值 NULL);
主键列中的值不允许修改或更新;
主键值不能重用(如果某行从表中删除,它的主键不能赋给以后的新行)。
主键通常定义在表的一列上,但并不是必须这么做,也可以一起使用多个列作为主键。在使用多列作为主键时,上述条件必须应用到所有列,所有列值的组合必须是唯一的(但其中单个列的值可以不唯一)。

3.2 数据库的种类

数据库共有2种类型:关系型数据库、非关系型数据库 。

3.2.1、关系数据库

MySQL、
MariaDB(MySQL的代替品)、
Percona Server(MySQL的代替品·)、
PostgreSQL、
Microsoft Access、
Google Fusion Tables、
SQLite、
DB2、
FileMaker、
Oracle、
SQL Server、
INFORMIX、
Sybase、
dBASE、
Clipper、
FoxPro、
foshub。
几乎所有的数据库管理系统都配备了一个开放式数据库连接(ODBC)驱动程序,令各个数据库之间得以互相集成。

3.2.2、非关系型数据库(NoSQL)

redis、
MongoDB、
Memcache、
HBase、
BigTable、
Cassandra、
CouchDB、
Neo4J。

3.3 关系型数据库和非关系型数据库的区别

3.3.1关系型数据库

关系型数据库最典型的数据结构是表,由二维表及其之间的联系所组成的一个数据组织

优点:
1、易于维护:都是使用表结构,格式一致;
2、使用方便:SQL语言通用,可用于复杂查询;
3、复杂操作:支持SQL,可用于一个表以及多个表之间非常复杂的查询。

缺点:
1、读写性能比较差,尤其是海量数据的高效率读写;
2、固定的表结构,灵活度稍欠;
3、高并发读写需求,传统关系型数据库来说,硬盘I/O是一个很大的瓶颈。

3.3.2 非关系型数据库

非关系型数据库严格上不是一种数据库,应该是一种数据结构化存储方法的集合,可以是文档或者键值对等。

优点:
1、格式灵活:存储数据的格式可以是key,value形式、文档形式、图片形式等等,文档形式、图片形式等等,使用灵活,应用场景广泛,而关系型数据库则只支持基础类型。
2、速度快:nosql可以使用硬盘或者随机存储器作为载体,而关系型数据库只能使用硬盘;
3、高扩展性;
4、成本低:nosql数据库部署简单,基本都是开源软件。

缺点:
1、不提供sql支持,学习和使用成本较高;
2、无事务处理;
3、数据结构相对复杂,复杂查询方面稍欠。

3.3.3 部分数据库的区别:

redis: (非关系型数据库----键值数据库)
MongoDB:(非关系型数据库----文档数据库)
HBase:(非关系型数据库----列存储数据库)
Neo4J : (非关系型数据库----图形数据库)

补充:
数据库模型:
对象模型、
层次模型(轻量级数据访问协议)、
网状模型(大型数据储存)、
关系模型、
面向对象模型、
半结构化模型、
平面模型(表格模型,一般在形式上是一个二维数组。如表格模型数据Excel)。

3.4mysql数据库

3.4.1. mysql的基本概念

数据库(database)就是一个存储数据库的仓库,为了方便数据的存储和管理,它将数据按照特定的规律存储在磁盘上。通过数据库管理系统,可以有效的组织和管理存储在数据库中的数据。mysql数据库就是这样一个关系型数据库管理系统(RDBMS),它可以称得上是目前运行速度最快的SQL数据库管理系统。

3.4.1.1 mysql的优势

mysql是一款自由软件,任何人可以从官网网址中下载。mysql是一个真正的多用户,多线程sql数据库服务器。它是以客户/服务器结构的实现,由一个服务程序mysqld和很多不同的客户程序和库组成。它能够快捷、有效和安全地处理大量的数据。相对于oracle等数据库来说,mysql在使用时非常简单。mysql的主要目标是快捷、便捷和易用

以下是mysql的一些特点:
1、开源和免费:
MySQL是开源的,并且可以免费使用。这使得MySQL成为开发人员和小型企业的首选数据库。
2、可扩展性:
MySQL可以轻松地扩展以支持更大的数据集和更高的并发用户。MySQL提供了许多扩展和插件,以帮助用户轻松地进行扩展。
3、跨平台支持:
MySQL可以在多个平台上运行,包括Windows、Linux、Unix、macOS等。
4、安全性:
MySQL具有强大的安全功能,可以保护数据免受未经授权的访问。MySQL提供了许多安全功能,例如加密、用户访问控制和审计等。
5、高可用性:
MySQL支持主从复制和自动故障转移,这使得MySQL可以实现高可用性和灾备恢复。
6、性能:
MySQL具有卓越的性能,可以轻松地处理大型数据集和高并发用户。MySQL支持索引、查询优化和缓存等功能,以提高性能。
7、支持大型企业应用程序:
MySQL可以处理大型企业级应用程序和高流量网站。许多大型企业和网站使用MySQL作为他们的数据库管理系统。

3.4.1.2 mysql的发展历史

MySQL是由瑞典公司MySQL AB开发的一种关系型数据库管理系统。其名字的由来是由MySQL AB的创始人之一Michael Widenius取的,最初取名为“My”(他的女儿的名字),加上“SQL”(结构化查询语言)的缩写组成。因此,MySQL这个名字代表着“我的SQL”,强调MySQL作为一个开源软件,可以根据用户的需求进行个性化的定制和修改。MySQL的名字既简单又易于记忆,因此在数据库市场上得到了广泛的认可和应用。

3.4.2. mysql的特性

mysql是一个真正的多用户、多线程sql数据库服务器。SQL是世界上最流行的和标准化的数据库语言。其特性如下:
使用C和C++语言编写,并使用了多种编译器进行测试,保证了源代码的可移植性。
支持多种操作系统,如Linux、Mac os、Windows等。
为多种编程语言提供了API。这些编程语言包括C、C++、Java、Perl、 PHP、Ruby、Eiffel、Tcl等。
支持多线程,充分利用CPU资源。
优化SQL查询算法,有效提高查询速度。
既能够作为一个单独的应用程序在客户端服务器网络环境中,也能够作为一个库而嵌入到其他软件中提供多语言支持,常见的编码如中文的GB2312、BIG5,日文的Shift_JIS等都可以作为数据数据的表名和数据列名。
提供TCP/IP、ODBC和JDBC等多种数据库连接途径。
提供用于管理、检查、优化数据库操作的管理工具。
可以处理拥有千万条记录的大型数据库。
目前的最新版本是mysql9.0版本。

3.4.2.1 mysql的安装

可以去官网下载
下载完成后可以在终端查看

3.4.2.2 数据库常用对象

在mysql数据库中,表、视图、存储过程和索引等具体存储数据或对数据进行操作的实体都被称为数据库对象,下面介绍几种常用的数据库对象:
1、表:
表是数据库中所有数据的数据库对象,由行和列组成,用于组织和存储数据。
2、字段:
表中的每列称为一个字段,字段具有自己的属性,如字段类型、字段大小等。其中,字段类型是字段中最重要的属性,它决定了字段能够存储那种数据。SQL规范支持5中基本类型的字段类型:字符型、文本型、数值型、逻辑型和日期时间类型。
3、索引:
索引是一个单独的、物理的数据库结构。它是依赖于表建立的,在数据库中索引使数据库程序无需对整个表进行扫描,就可以在其中找到所需要的数据。
4、视图:
视图是从一张表或者多张表中导出的表,是用户查看数据表中数据的一张方式。表中包括几个被定义的数据列与数据行,其结构和数据建立在对表的查询基础之上。
5、存储过程:
存储过程是一组为了完成特定功能的SQL语句集合(包含查询、插入、删除和更新等操作),经编译后以名称的形式存储在SQL sever服务器端端数据库中,由用户通过指定存储过程的名字来执行,当这个存储过程被调用执行时,这些操作也会同时执行。

3.4.3. mysql的存储引擎

MySQL的存储引擎(storage engine)是指用于存储和检索数据的底层软件组件。MySQL支持多种不同的存储引擎,每种引擎都有自己的特点和优势,应该根据具体需求选择合适的引擎。

以下是MySQL常见的存储引擎:
1、InnoDB
InnoDB是MySQL默认的存储引擎,支持ACID事务处理和行级锁定,适用于高并发的OLTP应用。它还支持外键约束和崩溃恢复等高级特性。
2、MyISAM
MyISAM是MySQL的传统存储引擎,不支持事务和行级锁定,但对于读多写少的应用非常适合。它的特点是速度快,占用资源少,适用于数据仓库和大型数据表。
3、Memory
Memory存储引擎将数据存储在内存中,速度非常快,但缺点是容易丢失数据,适用于临时数据表和缓存。它还支持HASH和B-tree索引。
4、CSV
CSV存储引擎将数据以CSV格式存储在文件中,适用于存储大量文本数据,但不支持索引和事务处理。
5、Archive
Archive存储引擎是一种高度压缩的存储引擎,适用于存储历史数据或归档数据,但不支持索引和事务处理。
6、Blackhole
Blackhole存储引擎不实际存储数据,所有写入操作都被丢弃,适用于数据复制和数据同步等应用场景。
7、Federated
Federated存储引擎可以将远程服务器上的表映射为本地表,支持跨服务器的查询和数据操作。
总的来说,选择适合自己应用场景的存储引擎可以提高数据库的性能和可靠性。

3.5mysql操作语句

3.5.1 数据库

# 查看所有的数据库
SHOW DATABASES ;
# 创建一个数据库
CREATE DATABASE k;
# 删除一个数据库
DROP DATABASE k;
# 使用这个数据库
USE k;

3.5.2 表

# 查看所有的表
SHOW TABLES ;
# 创建一个表
CREATE TABLE n(id INT, name VARCHAR(10));
CREATE TABLE m(id INT, name VARCHAR(10), PRIMARY KEY (id), FOREIGN KEY (id) REFERENCES n(id), UNIQUE (name));
CREATE TABLE m(id INT, name VARCHAR(10));
# 直接将查询结果导入或复制到新创建的表
CREATE TABLE n SELECT * FROM m;
# 新创建的表与一个存在的表的数据结构类似
CREATE TABLE m LIKE n;
# 创建一个临时表
# 临时表将在你连接MySQL期间存在。当断开连接时,MySQL将自动删除表并释放所用的空间。也可手动删除。
CREATE TEMPORARY TABLE l(id INT, name VARCHAR(10));
# 直接将查询结果导入或复制到新创建的临时表
CREATE TEMPORARY TABLE tt SELECT * FROM n;
# 删除一个存在表
DROP TABLE IF EXISTS m;
# 更改存在表的名称
ALTER TABLE n RENAME m;
RENAME TABLE n TO m;
# 查看表的结构(以下五条语句效果相同)
DESC n;   # 因为简单,所以建议使用
DESCRIBE n;
SHOW COLUMNS IN n;
SHOW COLUMNS FROM n;
EXPLAIN n;
# 查看表的创建语句
SHOW CREATE TABLE n;

3.5.3 表的结构

# 添加字段
ALTER TABLE n ADD age VARCHAR(2) ;
# 删除字段
ALTER TABLE n DROP age;
# 更改字段属性和属性
ALTER TABLE n CHANGE age a INT;
# 只更改字段属性
ALTER TABLE n MODIFY age VARCHAR(7) ;

3.5.4 表的数据

# 增加数据
INSERT INTO n VALUES (1, 'tom', '23'), (2, 'john', '22');
INSERT INTO n SELECT * FROM n;  # 把数据复制一遍重新插入
# 删除数据
DELETE FROM n WHERE id = 2;
# 更改数据
UPDATE n SET name = 'tom' WHERE id = 2;
# 数据查找
SELECT * FROM n WHERE name LIKE '%h%';
# 数据排序(反序)
SELECT * FROM n ORDER BY name, id DESC ;

3.5.5 键

# 添加主键
ALTER TABLE n ADD PRIMARY KEY (id);
ALTER TABLE n ADD CONSTRAINT pk_n PRIMARY KEY (id);   # 主键只有一个,所以定义键名似乎也没有什么用
# 删除主键
ALTER TABLE n DROP PRIMARY KEY ;
# 添加外键
ALTER TABLE m ADD FOREIGN KEY (id) REFERENCES n(id);    # 自动生成键名m_ibfk_1
ALTER TABLE m ADD CONSTRAINT fk_id FOREIGN KEY (id) REFERENCES n(id);   # 使用定义的键名fk_id
# 删除外键
ALTER TABLE m DROP FOREIGN KEY `fk_id`;
# 修改外键
ALTER TABLE m DROP FOREIGN KEY `fk_id`, ADD CONSTRAINT fk_id2 FOREIGN KEY (id) REFERENCES n(id);    # 删除之后从新建
# 添加唯一键
ALTER TABLE n ADD UNIQUE (name);
ALTER TABLE n ADD UNIQUE u_name (name);
ALTER TABLE n ADD UNIQUE INDEX u_name (name);
ALTER TABLE n ADD CONSTRAINT u_name UNIQUE (name);
CREATE UNIQUE INDEX u_name ON n(name);
# 添加索引
ALTER TABLE n ADD INDEX (age);
ALTER TABLE n ADD INDEX i_age (age);
CREATE INDEX i_age ON n(age);
# 删除索引或唯一键
DROP INDEX u_name ON n;
DROP INDEX i_age ON n;

3.5.6 视图

# 创建视图
CREATE VIEW v AS SELECT id, name FROM n;
CREATE VIEW v(id, name) AS SELECT id, name FROM n;
# 查看视图(与表操作类似)
SELECT * FROM v;
DESC v;
# 查看创建视图语句
SHOW CREATE VIEW v;
# 更改视图
CREATE OR REPLACE VIEW v AS SELECT name, age FROM n;
ALTER VIEW v AS SELECT name FROM n ;
# 删除视图
DROP VIEW IF EXISTS v;

3.5.7 联接

# 内联接
SELECT * FROM m INNER JOIN n ON m.id = n.id;
# 左外联接
SELECT * FROM m LEFT JOIN n ON m.id = n.id;
# 右外联接
SELECT * FROM m RIGHT JOIN n ON m.id = n.id;
# 交叉联接
SELECT * FROM m CROSS JOIN n;   # 标准写法
SELECT * FROM m, n;
# 类似全连接full join的联接用法
SELECT id,name FROM m
UNION
SELECT id,name FROM n;

3.5.8 函数

# 聚合函数
SELECT count(id) AS total FROM n;   # 总数
SELECT sum(age) AS all_age FROM n;   # 总和
SELECT avg(age) AS all_age FROM n;   # 平均值
SELECT max(age) AS all_age FROM n;   # 最大值
SELECT min(age) AS all_age FROM n;   # 最小值
# 数学函数
SELECT abs(-5);   # 绝对值
SELECT bin(15), oct(15), hex(15);   # 二进制,八进制,十六进制
SELECT pi();   # 圆周率3.141593
SELECT ceil(5.5);   # 大于x的最小整数值6
SELECT floor(5.5);   # 小于x的最大整数值5
SELECT greatest(3,1,4,1,5,9,2,6);   # 返回集合中最大的值9
SELECT least(3,1,4,1,5,9,2,6);    # 返回集合中最小的值1
SELECT mod(5,3);    # 余数2
SELECT rand();    # 返回0到1内的随机值,每次不一样
SELECT rand(5);   # 提供一个参数(种子)使RAND()随机数生成器生成一个指定的值。
SELECT round(1415.1415);   # 四舍五入1415
SELECT round(1415.1415, 3);   # 四舍五入三位数1415.142
SELECT round(1415.1415, -1);    # 四舍五入整数位数1420
SELECT truncate(1415.1415, 3);    # 截短为3位小数1415.141
SELECT truncate(1415.1415, -1);   # 截短为-1位小数1410
SELECT sign(-5);    # 符号的值负数-1
SELECT sign(5);    # 符号的值正数1
SELECT sqrt(9);   # 平方根3
SELECT sqrt(9);   # 平方根3
# 字符串函数
SELECT concat('a', 'p', 'p', 'le');   # 连接字符串-apple
SELECT concat_ws(',', 'a', 'p', 'p', 'le');   # 连接用','分割字符串-a,p,p,le
SELECT insert('chinese', 3, 2, 'IN');    # 将字符串'chinese'从3位置开始的2个字符替换为'IN'-chINese
SELECT left('chinese', 4);   # 返回字符串'chinese'左边的4个字符-chin
SELECT right('chinese', 3);   # 返回字符串'chinese'右边的3个字符-ese
SELECT substring('chinese', 3);   # 返回字符串'chinese'第三个字符之后的子字符串-inese
SELECT substring('chinese', -3);   # 返回字符串'chinese'倒数第三个字符之后的子字符串-ese
SELECT substring('chinese', 3, 2);   # 返回字符串'chinese'第三个字符之后的两个字符-in
SELECT trim(' chinese ');    # 切割字符串' chinese '两边的空字符-'chinese'
SELECT ltrim(' chinese ');    # 切割字符串' chinese '两边的空字符-'chinese '
SELECT rtrim(' chinese ');    # 切割字符串' chinese '两边的空字符-' chinese'
SELECT repeat('boy', 3);    # 重复字符'boy'三次-'boyboyboy'
SELECT reverse('chinese');    # 反向排序-'esenihc'
SELECT length('chinese');   # 返回字符串的长度-7
SELECT upper('chINese'), lower('chINese');    # 大写小写 CHINESE    chinese
SELECT ucase('chINese'), lcase('chINese');    # 大写小写 CHINESE    chinese
SELECT position('i' IN 'chinese');    # 返回'i'在'chinese'的第一个位置-3
SELECT position('e' IN 'chinese');    # 返回'i'在'chinese'的第一个位置-5
SELECT strcmp('abc', 'abd');    # 比较字符串,第一个参数小于第二个返回负数- -1
SELECT strcmp('abc', 'abb');    # 比较字符串,第一个参数大于第二个返回正数- 1
# 时间函数
SELECT current_date, current_time, now();    # 2018-01-13   12:33:43    2018-01-13 12:33:43
SELECT hour(current_time), minute(current_time), second(current_time);    # 12  31   34
SELECT year(current_date), month(current_date), week(current_date);   # 2018    1   1
SELECT quarter(current_date);   # 1
SELECT monthname(current_date), dayname(current_date);   # January  Saturday
SELECT dayofweek(current_date), dayofmonth(current_date), dayofyear(current_date);    # 7   13  13
# 控制流函数
SELECT if(3>2, 't', 'f'), if(3<2, 't', 'f');    # t f
SELECT ifnull(NULL, 't'), ifnull(2, 't');    # t 2
SELECT isnull(1), isnull(1/0);    # 0 1 是null返回1,不是null返回0
SELECT nullif('a', 'a'), nullif('a', 'b');    # null a 参数相同或成立返回null,不同或不成立则返回第一个参数
SELECT CASE 2
       WHEN 1 THEN 'first'
       WHEN 2 THEN 'second'
       WHEN 3 THEN 'third'
       ELSE 'other'
       END ;     # second
# 系统信息函数
SELECT database();    # 当前数据库名-test
SELECT connection_id();   # 当前用户id-306
SELECT user();    # 当前用户-root@localhost
SELECT version();   # 当前mysql版本
SELECT found_rows();    # 返回上次查询的检索行数

3.5.9 用户

# 增加用户
CREATE USER 'test'@'localhost' IDENTIFIED BY 'test';
INSERT INTO mysql.user(Host, User, Password) VALUES ('localhost', 'test', Password('test'));    # 在用户表中插入用户信息,直接操作User表不推荐
# 删除用户
DROP USER 'test'@'localhost';
DELETE FROM mysql.user WHERE User='test' AND Host='localhost';
FLUSH PRIVILEGES ;
# 更改用户密码
SET PASSWORD FOR 'test'@'localhost' = PASSWORD('test');
UPDATE mysql.user SET Password=Password('t') WHERE User='test' AND Host='localhost';
FLUSH PRIVILEGES ;
# 用户授权
GRANT ALL PRIVILEGES ON *.* TO test@localhost IDENTIFIED BY 'test';
# 授予用'test'密码登陆成功的test@localhost用户操作所有数据库的所有表的所有的权限
FLUSH PRIVILEGES ;   # 刷新系统权限表,使授予权限生效
# 撤销用户授权
REVOKE DELETE ON *.* FROM 'test'@'localhost';   # 取消该用户的删除权限

3.5.10存储过程

# 创建存储过程
DELIMITER //    # 无参数
CREATE PROCEDURE getDates()
  BEGIN
    SELECT * FROM test ;
  END //
CREATE PROCEDURE getDates_2(IN id INT)    # in参数
  BEGIN
    SELECT * FROM test WHERE a = id;
  END //
CREATE PROCEDURE getDates_3(OUT sum INT)    # out参数
  BEGIN
    SET sum = (SELECT count(*) FROM test);
  END //
CREATE PROCEDURE getDates_4(INOUT i INT)    # inout参数
  BEGIN
    SET i = i + 1;
  END //
DELIMITER ;
# 删除存储过程
DROP PROCEDURE IF EXISTS getDates;
# 修改存储过程的特性
ALTER PROCEDURE getDates MODIFIES SQL DATA ;
# 修改存储过程语句(删除再重建)略
# 查看存储过程
SHOW PROCEDURE STATUS LIKE 'getDates';    # 状态
SHOW CREATE PROCEDURE getDates_3;   # 语句
# 调用存储过程
CALL getDates();
CALL getDates_2(1);
CALL getDates_3(@s);
SELECT @s;
SET @i = 1;
CALL getDates_4(@i);
SELECT @i;    # @i = 2

3.5.11 其他语句

# 查看所有的表信息(包括视图)
SHOW TABLE STATUS;

3.5.12 其他

# 数据库备份
mysqldump -u root -p db_name > file.sql
mysqldump -u root -p db_name table_name > file.sql
# 数据库还原
mysql -u root -p < C:\file.sql

4.阿里云

4.1 什么是服务器

服务器是一种专用的计算机或系统软件,旨在提供各种网络服务,如数据存储、文件共享、电子邮件、数据库管理和互联网访问等。服务器通常具有较高的处理能力、存储容量和网络带宽,以满足多个客户端(如个人电脑、移动设备或其他服务器)的需求。

4.2 购买阿里云服务器

阿里云官网阿里云-计算,为了无法计算的价值 (aliyun.com)
登陆进入官网,选择产品-云服务器,选择云服务器ECS。
在这里插入图片描述

4.3 服务器选择操作系统

按照自己的需求购买服务器ECS即可。这里我实例规格选择的是基础配置(2vCPU2GiB),镜像选择的是Ubuntu22.04 64位,按需选择即可。
在这里插入图片描述

4.4 访问操作系统

4.4.1 xshell进行访问

前提条件

​ 1.阿里云 ECS 服务器的 IP 地址

​ 2.服务器的用户名和密码SSH 密钥对

​ 3.Xshell 软件(已安装在你的电脑上)

步骤:
​ 1.打开 Xshell
​ 打开已经安装的 Xshell 软件。
在这里插入图片描述
​ 2.创建新会话
在 Xshell 主界面,点击左上角的“新建”按钮,创建一个新的会话。
在这里插入图片描述
​ 3.配置会话
在弹出的“新建会话属性”窗口中,配置以下信息:
名称:为会话起一个名字(比如“阿里云 ECS”)。
主机:输入阿里云 ECS 服务器的公有 IP 地址。(在阿里云服务器-控制台-配置信息中)
在这里插入图片描述
协议:选择 SSH。
​ •端口号:默认情况下,SSH 的端口号是 22。
在这里插入图片描述
​ 4.用户身份验证
​ 切换到“用户身份验证”标签,配置以下信息:
​ •用户名:输入你登录服务器的用户名(默认一般是 root 或者其他你设置的用户名)。
​ •认证方法
•如果使用密码登录,选择“密码”,然后输入你的密码。
•如果使用密钥对登录,选择“公钥”,并点击“设置”按钮选择你的私钥文件。
在这里插入图片描述
​ 5.保存设置并连接
点击“确定”保存会话设置,然后在 Xshell 主界面中双击刚创建的会话,开始连接到服务器。
​ 6.接受主机密钥
​ 第一次连接时,Xshell 会提示你接受服务器的主机密钥。确认信息无误后,点击“接受并保存”按钮。
在这里插入图片描述
​ 7.成功连接
​ 如果所有配置正确,你将会成功连接到阿里云 ECS 服务器,并看到服务器的命令行提示符。现在即可在 Xshell 中输入命令并对服务器进行操作。
在这里插入图片描述

4.4.2 网址访问

再购买实例后,可以通过网址对操作系统进行访问。
在这里插入图片描述
如图所示,点击控制台,进入控制台后,可以看到购买的实例。
在这里插入图片描述
点击远程连接-通过Workbench远程连接,即可访问实例。
在这里插入图片描述

5.部署mysql

5.1命令安装mysql

5.1.1 更新包列表并安装 MySQL

​ 1.更新包列表:sudo apt update
​ 2.安装 MySQL 服务器:sudo apt install mysql-server

5.1.2 启动 MySQL 服务并检查状态

​ 1.确认 MySQL 服务已经启动:sudo systemctl status mysql

5.1.3 运行 MySQL 安全安装脚本

1.配置 MySQL 安全设置:sudo mysql_secure_installation
​ 按照提示完成以下配置:
​ •设置 root 用户密码
​ •移除匿名用户
​ •禁止 root 用户远程登录
​ •删除测试数据库
​ •重新加载权限表

5.2对数据库进行配置

5.2.1 编辑 MySQL 配置文件

1.打开 MySQL 配置文件进行编辑:sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf

5.2.2 修改基本配置选项

​ 1.在 mysqld.cnf 文件中,根据需要添加或修改以下配置选项:

[mysqld]
port = 3306
bind-address = 0.0.0.0  # 如果需要远程访问,请将此值设为 0.0.0.0 或具体的 IP 地址
max_connections = 100
default-storage-engine = INNODB
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci

5.2.3 重启 MySQL 服务并创建用户和数据库

​ 1.重启 MySQL 服务使配置生效:sudo systemctl restart mysql
​ 2.通过 MySQL 命令行客户端连接到数据库并创建新的用户和数据库:sudo mysql -u root -p
​ 3.在 MySQL 提示符下运行以下命令创建数据库和用户:

CREATE DATABASE your_database_name;
CREATE USER 'your_username'@'localhost' IDENTIFIED BY 'your_password';
GRANT ALL PRIVILEGES ON your_database_name.* TO 'your_username'@'localhost';
FLUSH PRIVILEGES;

6.部署区块链

6.1前期环境配置

1.Java安装

$ sudo add-apt-repository ppa:openjdk-r/ppa
$ sudo apt-get update
$ sudo apt-get install openjdk-8-jdk
# 查询Java版本
$ java -version

2.安装MySQL数据库(建议安装MySQL5)

​ 2.1添加MySQL源

sudo vim /etc/apt/sources.list.d/mysql.list
添加如下内容:

deb http://repo.mysql.com/apt/ubuntu/ bionic mysql-apt-config
deb http://repo.mysql.com/apt/ubuntu/ bionic mysql-5.7
deb http://repo.mysql.com/apt/ubuntu/ bionic mysql-tools
deb-src http://repo.mysql.com/apt/ubuntu/ bionic mysql-5.7

​ 更新
sudo apt update
​ 执行过程中可能会出现如下错误:

W: GPG error: http://repo.mysql.com/apt/ubuntu bionic InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 467B942D3A79BD29
E: The repository 'http://repo.mysql.com/apt/ubuntu bionic InRelease' is not signed.
N: Updating from such a repository can't be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.

​ 执行如下命令即可,467B942D3A79BD29根据实际情况替换:
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 467B942D3A79BD29
​ 重新执行更新:
sudo apt update

​ 2.2安装 MySQL

​ 查看支持的 MySQL 版本:sudo apt-cache policy mysql-server
​ 安装 MySQL 5.7 版本客户端和服务端:sudo apt install mysql-client=5.7.40-1ubuntu18.04

3.Python安装

// 添加仓库,回车继续
sudo add-apt-repository ppa:deadsnakes/ppa
// 安装python 3.6
sudo apt-get install -y python3.6
sudo apt-get install -y python3-pip

4.PyMySQL安装

sudo apt-get install -y python3-pip
sudo pip3 install PyMySQL

在这里插入图片描述

6.2区块链安装

6.2.1下载包

1.获取部署安装包

wget https://osp-1257653870.cos.ap-guangzhou.myqcloud.com/WeBASE/releases/download/v1.5.5/webase-deploy.zip

2.解压安装包

apt install unzip
unzip webase-deploy.zip

3.进入目录

cd webase-deploy

4.修改配置(如图所示)

vi common.properties

在这里插入图片描述

6.2.2一键部署

# 部署并启动所有服务
python3 deploy.py installAll

看到如下提示即表示部署成功:

...
============================================================
              _    _     ______  ___  _____ _____ 
             | |  | |    | ___ \/ _ \/  ___|  ___|
             | |  | | ___| |_/ / /_\ \ `--.| |__  
             | |/\| |/ _ | ___ |  _  |`--. |  __| 
             \  /\  |  __| |_/ | | | /\__/ | |___ 
              \/  \/ \___\____/\_| |_\____/\____/  
...
...
============================================================
==============      deploy  has completed     ==============
============================================================
==============    webase-web version  v1.5.5        ========
==============    webase-node-mgr version  v1.5.5   ========
==============    webase-sign version  v1.5.3       ========
==============    webase-front version  v1.5.5      ========
============================================================

若出现如下错误:error! JAVA_HOME has not been configured!
在这里插入图片描述
解决:

cd /usr/lib/jvm/
ls
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
export PATH=${JAVA_HOME}/bin:${JRE_HOME}/bin:$PATH
echo $JAVA_HOME

重新运行python3 deploy.py installAll
服务部署后,需要对各服务进行启停操作,可以使用以下命令:

部署并启动所有服务        python3 deploy.py installAll
停止一键部署的所有服务    python3 deploy.py stopAll
启动一键部署的所有服务    python3 deploy.py startAll# 各子服务启停
启动FISCO-BCOS节点:      python3 deploy.py startNode
停止FISCO-BCOS节点:      python3 deploy.py stopNode
启动WeBASE-Web:          python3 deploy.py startWeb
停止WeBASE-Web:          python3 deploy.py stopWeb
启动WeBASE-Node-Manager: python3 deploy.py startManager
停止WeBASE-Node-Manager: python3 deploy.py stopManager
启动WeBASE-Sign:        python3 deploy.py startSign
停止WeBASE-Sign:        python3 deploy.py stopSign
启动WeBASE-Front:        python3 deploy.py startFront
停止WeBASE-Front:        python3 deploy.py stopFront# 可视化部署
部署并启动可视化部署的所有服务  python3 deploy.py installWeBASE
停止可视化部署的所有服务  python3 deploy.py stopWeBASE
启动可视化部署的所有服务  python3 deploy.py startWeBASE

6.2.3区块链配置

区块链配置文件为common.properties
在终端输入以下命令进入区块链配置文件:

cd webase-deploy
vi common.properties

在这里插入图片描述

在node.counts中可以修改区块链的节点个数(默认为2个)。

6.2.4开放端口

webase平台的访问地址为:http://localhost:5000
使用云服务厂商的服务器时,需要开通网络安全组的对应端口。如开放webase使用的5000端口。
在阿里云服务器中开放5000端口
在这里插入图片描述
在服务器控制台中,点击安全组,进入安全组。
在这里插入图片描述
选择管理规则,手动添加5000端口。
在这里插入图片描述
通过云服务器的公网ip:5000即可访问webase平台。
在这里插入图片描述

6.3项目部署

6.3.1编写智能合约

在Webase管理平台编写智能合约VotingSystem.sol智能合约。在VotingSystem智能合约中,该合约负责存储和检索投票记录,本质上充当了投票详细信息的数据库或账本。合约允许存储与特定活动相关的投票,并提供按投票标识符或活动名称检索这些投票的功能。以下是合约功能的描述:
1.投票存储和检索:合约存储投票记录,包括消息、投票本身、投票的SM3哈希值、投票人的公钥以及与投票相关的活动名称。这些详细信息封装在一个VoteRecord结构体中。
2.基于活动的投票管理:合约维护活动名称与相关投票之间的映射关系,这样可以将特定活动下的所有投票进行分组并检索。
3.功能:
•storeVote:此函数用于将新投票存储到系统中。它接收投票详细信息作为输入,并将投票与相关活动相关联。
•getVoteByVote:此函数通过投票标识符检索特定投票记录,并返回与该投票相关的所有详细信息。
•getVotesByActivityName:此函数允许用户检索与特定活动名称相关的所有投票记录,并返回包含该活动下所有投票详细信息的 VoteRecord 结构体数组。
在这里插入图片描述

AdvancedVotingSystem.sol代码如下:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
pragma experimental ABIEncoderV2;

contract AdvancedVotingSystem {
    struct VoteRecord {
        string vote;
        string sm3_vote;
        string ringSign_publicKey;
        string vote_time;
        string publicKey_id;
        string activity_name;
    }

    mapping(string => VoteRecord) private votes;
    mapping(string => string[]) private activityVotes;

    // 存储投票记录
    function storeVote(
        string memory _vote,
        string memory _sm3_vote,
        string memory _ringSign_publicKey,
        string memory _vote_time,
        string memory _publicKey_id,
        string memory _activity_name
    ) public {
        VoteRecord memory newVote = VoteRecord({
            vote: _vote,
            sm3_vote: _sm3_vote,
            ringSign_publicKey: _ringSign_publicKey,
            vote_time: _vote_time,
            publicKey_id: _publicKey_id,
            activity_name: _activity_name
        });

        votes[_vote] = newVote;
        activityVotes[_activity_name].push(_vote);
    }


    // 通过 vote 查询对应的投票记录
    function getVoteByVote(string memory _vote) public view returns (string memory, string memory, string memory, string memory, string memory, string memory) {
        require(bytes(votes[_vote].vote).length != 0, "Vote not found");
        VoteRecord memory voteRecord = votes[_vote];
        return (voteRecord.vote, voteRecord.sm3_vote, voteRecord.ringSign_publicKey, voteRecord.vote_time, voteRecord.publicKey_id, voteRecord.activity_name);
    }

    // 通过 activity_name 查询所有该 activity_name 下的投票记录
    function getVotesByActivityName(string memory _activity_name) public view returns (VoteRecord[] memory) {
        string[] memory voteKeys = activityVotes[_activity_name];
        VoteRecord[] memory results = new VoteRecord[](voteKeys.length);

        for (uint i = 0; i < voteKeys.length; i++) {
            results[i] = votes[voteKeys[i]];
        }

        return results;
    }
}

在合约编写完毕之后,接下来便是对合约进行编译的环节。编译完成后,将生成的字节码发送至区块链网络中的相关节点,执行这一操作会触发交易的发送。随着交易的成功,合约正式部署在区块链上,这时就可以进行选票信息的存储与查询操作。通过这种方式,确保选票信息的高效、安全存储,并且可以随时通过合约查询到相关信息,从而保证了电子投票系统的透明性和可信度。
在这里插入图片描述

在完成合约编译并发送交易之后,可看到交易回执。这一回执提供了对交易状态的详细记录。通过分析回执中的数据,我们能够确认选票信息是否已成功存储在区块链上,并获得相关的交易细节。这一过程不仅增强了系统的透明度,还为后续的数据查询和验证提供了坚实的基础。
在这里插入图片描述
在查询投票记录的过程中,首先需要通过投票标识符调用合约中的查询函数。该函数会根据传入的标识符在合约中查找对应的投票记录。查询操作完成后,合约会返回与该投票记录相关的所有详细信息,如消息内容、投票信息、SM3 哈希值、公钥以及活动名称等。
在这里插入图片描述
通过这种方式,用户能够快速、高效地获取特定投票的详细信息,确保所有投票记录都能被准确地存储和查询,从而进一步增强电子投票系统的透明度和可信度。在完成查询操作后,用户将获得一组与投票记录相关的返回值,这些返回值提供了对投票数据的完整描述。通过分析这些返回值,用户可以确认投票记录是否正确存储在区块链上,并获取相关的详细信息。这一过程不仅保证了投票记录的可追溯性,还为后续的审计和验证提供了有力支持。
在这里插入图片描述
在通过活动名称查询所有关联的投票记录时,需要调用合约中的相应查询函数。该函数会根据传入的活动名称在合约中查找所有与之相关的投票标识符,并进一步检索每个投票记录的详细信息。查询操作完成后,合约会返回与该活动名称相关的所有投票记录的数组,其中包含每条投票记录的完整信息。
在这里插入图片描述
通过这种方式,用户可以轻松地获取特定活动下的所有投票信息,确保每一条投票记录都能被正确分类和检索,提升电子投票系统的操作效率和信息管理能力。在查询完成后,用户将收到一个包含多个投票记录的返回数组,这些数组提供了该活动下所有投票的详细情况。通过分析这些返回结果,用户可以全面掌握该活动的投票情况,为后续的统计、分析和验证提供准确的数据支持。这一过程有效地保证了投票数据的完整性和可靠性。
在这里插入图片描述
用户可以通过输入活动名称来查询该活动下所有候选人的得票数据。合约中的查询函数会根据活动名称自动检索与之相关的所有投票记录,并对每个候选人的得票数进行统计。以下是计票智能合约代码,展示了这一统计过程:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
pragma experimental ABIEncoderV2;

contract VotingSystem3 {
    struct VoteRecord {
        string activityName;
        string vote;
        string candidateName;
    }

    // 存储每个活动对应的所有投票记录
    mapping(string => VoteRecord[]) private activityVotes;
    // 存储每个活动下每个候选人对应的投票数量
    mapping(string => mapping(string => uint)) private candidateVoteCount;
    // 存储每个活动的总投票数
    mapping(string => uint) private totalVotesCount;

    // 存储投票记录
    function storeVote(
        string memory _activityName,
        string memory _vote,
        string memory _candidateName
    ) public {
        VoteRecord memory newVote = VoteRecord({
            activityName: _activityName,
            vote: _vote,
            candidateName: _candidateName
        });

        activityVotes[_activityName].push(newVote);
        candidateVoteCount[_activityName][_candidateName]++;
        totalVotesCount[_activityName]++;
    }

    // 根据活动名称查询候选人的票数,并按得票数排序
    function getVotesByActivityName(string memory _activityName) public view returns (string memory) {
        uint totalVotes = totalVotesCount[_activityName];
        require(totalVotes > 0, "未找到该活动的投票记录");

        string memory result = string(abi.encodePacked("活动名称: ", _activityName, "\n总投票数: ", uintToString(totalVotes), "\n\n"));

        // 统计候选人及其票数
        string[] memory candidates = new string[](activityVotes[_activityName].length);
        uint[] memory votesCount = new uint[](activityVotes[_activityName].length);
        uint candidateIndex = 0;

        for (uint i = 0; i < activityVotes[_activityName].length; i++) {
            string memory candidateName = activityVotes[_activityName][i].candidateName;
            
            // 检查是否已记录该候选人
            bool alreadyRecorded = false;
            for (uint j = 0; j < candidateIndex; j++) {
                if (keccak256(bytes(candidates[j])) == keccak256(bytes(candidateName))) {
                    alreadyRecorded = true;
                    break;
                }
            }
            
            // 如果未记录,则添加到数组中
            if (!alreadyRecorded) {
                candidates[candidateIndex] = candidateName;
                votesCount[candidateIndex] = candidateVoteCount[_activityName][candidateName];
                candidateIndex++;
            }
        }

        // 按票数排序
        for (uint i = 0; i < candidateIndex; i++) {
            for (uint j = i + 1; j < candidateIndex; j++) {
                if (votesCount[i] < votesCount[j]) {
                    // 交换票数
                    uint tempVotes = votesCount[i];
                    votesCount[i] = votesCount[j];
                    votesCount[j] = tempVotes;
                    // 交换候选人名字
                    string memory tempCandidate = candidates[i];
                    candidates[i] = candidates[j];
                    candidates[j] = tempCandidate;
                }
            }
        }

        // 生成最终结果
        for (uint i = 0; i < candidateIndex; i++) {
            string memory percentage = calculatePercentage(votesCount[i], totalVotes);
            result = string(abi.encodePacked(result, "候选人: ", candidates[i], "\n票数: ", uintToString(votesCount[i]), "\n得票率: ", percentage, "%\n\n"));
        }

        return result;
    }

    // 计算百分比
    function calculatePercentage(uint _part, uint _whole) internal pure returns (string memory) {
        uint percentage = (_part * 100) / _whole;
        return uintToString(percentage);
    }

    // uint 转换为 string
    function uintToString(uint v) internal pure returns (string memory) {
        if (v == 0) {
            return "0";
        }
        uint maxlength = 100;
        bytes memory reversed = new bytes(maxlength);
        uint i = 0;
        while (v != 0) {
            uint remainder = v % 10;
            v = v / 10;
            reversed[i++] = byte(uint8(48 + remainder));
        }
        bytes memory s = new bytes(i);
        for (uint j = 0; j < i; j++) {
            s[j] = reversed[i - j - 1];
        }
        return string(s);
    }
}

在这里插入图片描述

完成候选人得票数据查询后,用户将收到一份交易回执。回执中详细记录了查询过程中涉及的所有操作步骤和返回结果。每位候选人的得票数及其统计信息都将在回执中展示,确保用户能够全面掌握查询结果的细节。这份回执不仅为用户提供了交易的透明记录,还为后续的数据审核和验证提供了可靠依据,进一步增强了投票系统的可信度和安全性。
在这里插入图片描述
在Webase平台上,开发者可以通过合约的多种功能实现复杂的业务逻辑,例如投票系统中的投票记录存储与查询。平台的强大功能确保了数据的安全性和透明性,每一步操作都在区块链上得到精确记录和验证。通过交易回执的分析,开发者可以确认每笔交易的成功与否,并获取详细的交易信息。这种全流程的管理方式不仅提高了系统的可靠性,还为后续的运维和审计提供了有力保障。
在这里插入图片描述

6.3.2编译运行

在webase平台上点击编译,并点击部署,即部署成功,部署成功后,点击发交易即可进行存储与读取。再编译和部署完智能合约后,可以得到合约的地址。
在这里插入图片描述

6.3.3导出项目

1.点击右上角导出Java项目后,即可导出一个自命名的Java项目
在这里插入图片描述
2.在IDEA中打开导出的Java项目
项目结构如图:
在这里插入图片描述
这是一个gradle构建的SpringBoot项目。
3.编写Controller层来测试智能合约中的方法
TestController.java测试合约的add和get方法是否调用成功

package org.example.demo2.controller;


import org.example.demo2.model.bo.VoteAddCandidateInputBO;
import org.example.demo2.model.bo.VoteGetCandidateInfoInputBO;
import org.example.demo2.service.VoteService;
import org.fisco.bcos.sdk.v3.transaction.model.dto.CallResponse;
import org.fisco.bcos.sdk.v3.transaction.model.dto.TransactionResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {


    @Autowired
    private VoteService voteService;

    @PostMapping("/addCandidate")
    public TransactionResponse set(@RequestBody VoteAddCandidateInputBO vai) throws Exception {

        TransactionResponse result = voteService.addCandidate(vai);

        return result;
    }

    @GetMapping("/getCandidateInfo")
    public CallResponse get(@RequestBody VoteGetCandidateInfoInputBO vgi) throws Exception {
        CallResponse response = voteService.getCandidateInfo(vgi);

        return response;
    }
}

在这里插入图片描述
4.启动项目
项目启动成功如图所示:
在这里插入图片描述
5.Postman测试add和get方法
下图为add方法:
在这里插入图片描述
下图为get方法:
在这里插入图片描述
调用方法成功。

7.连接mysql

7.1navicat连接本地mysql

打开navicat,点击连接,选择mysql
在这里插入图片描述
输入账号和密码
在这里插入图片描述
点击测试
在这里插入图片描述
如果成功,则点击确定,连接成功

7.2navicat利用ssh连接服务器mysql

打开navicat,点击连接,选择mysql
在这里插入图片描述
点击ssh,输入云账号和密码
在这里插入图片描述
点击连接测试
在这里插入图片描述
如果navicat到ssh可以连通,说明ssh连接成功
点击常规,输入云上数据库和密码进行连接
在这里插入图片描述
点击测试
在这里插入图片描述
如果成功,则点击确定,连接成功

7.3springboot连接本地myaql

配置application.properties

#加载驱动
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库ip和端口
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/voteDataBase?useUnicode=true&useSSL=false&allowLoadLocalInfile=false&autoReconnect=true&failOverReadOnly=false&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=GMT%2B8&connectTimeout=1000&socketTimeout=30000
#账号
spring.datasource.username=root
#密码
spring.datasource.password=123456

7.4springboot利用ssh连接服务器mysql

7.4.1添加依赖

<dependency>
    <groupId>com.jcraft</groupId>
    <artifactId>jsch</artifactId>
    <version>0.1.55</version>
</dependency>

7.4.2编写配置类

SshConfiguration.java

package com.mc.aliyun;


import com.mc.aliyun.SshProperties;
import jakarta.annotation.PreDestroy;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.weaving.LoadTimeWeaverAware;
import org.springframework.instrument.classloading.LoadTimeWeaver;

import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;


import lombok.extern.slf4j.Slf4j;

@Configuration
@EnableConfigurationProperties(SshProperties.class)
@ConditionalOnProperty(prefix = "ssh", value = "enabled", havingValue = "true", matchIfMissing = false)
@Slf4j
// 实现 LoadTimeWeaverAware 接口是因为需要 SSH 正向代理需要在EntityManagerFactory加载前运行
public class SshConfiguration implements LoadTimeWeaverAware {

    private final Session session;

    public SshConfiguration(SshProperties sshProperties) {
        Session session = null;
        try {
            // 可以自行为 JSch 添加日志,需要实现 com.jcraft.jsch.Logger 接口
            // JSch.setLogger(new JSchLogger())
            session = new JSch().getSession(sshProperties.getUsername(), sshProperties.getHost(), sshProperties.getPort());
            session.setConfig("StrictHostKeyChecking", "no");
            session.setPassword(sshProperties.getPassword());
            session.connect();
            SshProperties.Forward forward = sshProperties.getForward();
            if (forward != null) {
                session.setPortForwardingL(forward.getFromHost(), forward.getFromPort(), forward.getToHost(), forward.getToPort());
                log.info("{}:{} -> {}:{}", forward.getFromHost(), forward.getFromPort(), forward.getToHost(), forward.getToPort());
            }
        } catch (JSchException e) {
            log.error("ssh " + sshProperties.getHost() + " failed.", e);
        }
        this.session = session;
    }

    @PreDestroy
    // 配置销毁时,断开 SSH 链接
    public void disconnect() {
        if (session != null) {
            session.disconnect();
        }
    }

    @Override
    public void setLoadTimeWeaver(LoadTimeWeaver loadTimeWeaver) {

    }

}

SshProperties.java

package com.mc.aliyun;

import org.springframework.boot.context.properties.ConfigurationProperties;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString(exclude="password")
@ConfigurationProperties(prefix="ssh")
public class SshProperties {

    private String host;
    private Integer port;
    private String username;
    private String password;
    private Forward forward;

    @Getter
    @Setter
    @ToString
    public static class Forward {

        private String fromHost;
        private Integer fromPort;
        private String toHost;
        private Integer toPort;

    }

}

7.4.3 编写配置文件application.properties

#加载驱动
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#连接数据库
spring.datasource.url=jdbc:mysql://127.0.0.1:3307/voteDataBase?useUnicode=true&useSSL=false&allowLoadLocalInfile=false&autoReconnect=true&failOverReadOnly=false&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=GMT%2B8&connectTimeout=1000&socketTimeout=30000
#spring.datasource.url=jdbc:mysql://127.0.0.1:3306/gm
#账号
spring.datasource.username=root
#密码
spring.datasource.password=123456

#开启ssh配置
ssh.enabled=true
#服务器地址
ssh.host=***.***.***.***
#连接服务器端口号
ssh.port=22
#服务器账号
ssh.username=root
#服务器密码
ssh.password=Zzh123456
#接收到数据库地址
ssh.forward.from_host=localhost
#接收到数据库端口
ssh.forward.from_port=3307
#来自于数据库地址
ssh.forward.to_host=localhost
#来自于数据库端口
ssh.forward.to_port=3306

7.3.4 原理

在这里插入图片描述服务器上数据库3306端口会映射到本机3307端口上

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值