整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

如何使用零知识证明在以太坊区块链上构建匿名投票系统

如何使用零知识证明在以太坊区块链上构建匿名投票系统

已经开始处理零知识证明技术,因为我很好奇是否有可能在区块链上创建一个匿名的、不可破解的投票系统。

纸质投票(如选举)是一种非常昂贵的投票方式。我没有确切的数据,但它花费了数十亿美元,而且一直有传言说有人作弊。

基于区块链的系统成本只有这个价格的一小部分,而且它超级安全,因为区块链是公开的,每个人都可以跟踪它的所有内容。

由于零知识证明,唯一无法追踪的是谁投票给了哪个政党(更详细的解释,请阅读我之前的文章)。因此,基于区块链的投票似乎是投票的圣杯。

在我之前的文章中,我承诺了一个简单的概念证明。它现在已经完成并可以在GitHub上使用。我将在本文中向您展示它是如何工作的。

我用的方法和龙卷风现金的方法是一样的。Tornado Cash是基于zkSNARKs的非托管以太坊和ERC20隐私解决方案。

它的主要组成部分是一个智能合约,你可以把你的钱存入一个承诺,你可以在以后通过一个撤销器提取。一个承诺只分配给一个无效符,但是没有人知道哪个无效符分配给哪个承诺。

这也可用于匿名投票,因为每个人只投一次票,但没有人应该知道哪张票分配给了哪位选民。(我有一篇关于这个话题的完整文章。)

我的投票应用是一个以太坊dApp。它是一个带有JavaScript的静态页面,没有任何后端(后端是以太坊上的智能合约)。正因为如此,很难攻击系统,因为在区块链上,没有单点故障。

如果您想在本地测试它,最简单的方法是通过以下方式运行本地区块链:

npx hardhat node

在MetaMask上注册账户我们将使用前两个帐户。

第一步,克隆repo,部署智能合约,并启动应用程序:

git clone https://github.com/TheBojda/zktree-vote
cd zktree-vote
npm i
npm run prepare (optional, npm i should run it)
npm run deploy
npm start

dApp使用MetaMask来与区块链通信,所以如果你从桌面浏览器使用它,MetaMask必须安装,如果你从手机使用它,在MetaMask应用程序的嵌入式浏览器中打开它。

当你打开http://localhost:1234/,你会看到应用程序的菜单:


点击“registration to vote”打开注册页面。当您打开页面时,它会在后台自动生成提交和无效符,并将其存储在浏览器的本地存储中。

选民的确认可以在投票地点亲自进行,也可以通过视频会议系统等在网上进行。验证器可以使用“Validator tool”:

为了方便起见,我在页面上添加了一个QR阅读器,因此可以通过扫描选民的QR码来发送承诺给验证者,或者选民可以复制它并在聊天中发送(在线验证)。

当验证者通过检查她的身份证等来检查选民的身份时,他将具有唯一哈希值的承诺发送给区块链,该哈希值可以是用户身份证或任何唯一标识符的哈希值。

这确保了一个选民只登记一次,承诺一次,这意味着她只能投票一次。

投票过程很简单。你选择一个选项并将其发送到区块链。应用程序将使用在后台生成的nullifier。

在最后一页,您可以实时查看投票结果。

就是这样。简单和非常舒适的投票方式,但它有纸质投票的所有优点,没有所有的缺点。

在这个简短的介绍之后,让我们来看一些代码。

为了开发这个投票dApp,我使用了我的zk-merkel-tree库。它是一个JavaScript库,使用Tornado Cash的基于zksnark的方法,并隐藏了零知识证明的所有复杂性。

我计划写一篇关于这个库的完整文章,所以在本文中,我不会详细介绍它。

投票系统的智能合约是这样的:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "zk-merkle-tree/contracts/ZKTree.sol";

contract ZKTreeVote is ZKTree {
    address public owner;
    mapping(address=> bool) public validators;
    mapping(uint256=> bool) uniqueHashes;
    uint numOptions;
    mapping(uint=> uint) optionCounter;

    constructor(
        uint32 _levels,
        IHasher _hasher,
        IVerifier _verifier,
        uint _numOptions
    ) ZKTree(_levels, _hasher, _verifier) {
        owner=msg.sender;
        numOptions=_numOptions;
        for (uint i=0; i <=numOptions; i++) optionCounter[i]=0;
    }

    function registerValidator(address _validator) external {
        require(msg.sender==owner, "Only owner can add validator!");
        validators[_validator]=true;
    }

    function registerCommitment(
        uint256 _uniqueHash,
        uint256 _commitment
    ) external {
        require(validators[msg.sender], "Only validator can commit!");
        require(
            !uniqueHashes[_uniqueHash],
            "This unique hash is already used!"
        );
        _commit(bytes32(_commitment));
        uniqueHashes[_uniqueHash]=true;
    }

    function vote(
        uint _option,
        uint256 _nullifier,
        uint256 _root,
        uint[2] memory _proof_a,
        uint[2][2] memory _proof_b,
        uint[2] memory _proof_c
    ) external {
        require(_option <=numOptions, "Invalid option!");
        _nullify(
            bytes32(_nullifier),
            bytes32(_root),
            _proof_a,
            _proof_b,
            _proof_c
        );
        optionCounter[_option]=optionCounter[_option] + 1;
    }

    function getOptionCounter(uint _option) external view returns (uint) {
        return optionCounter[_option];
    }
}

智能合约继承自ZKTree(来自zk-merkel-tree库),并使用它的_commit和_nullify方法。_commit方法存储承诺,_nullify方法存储无效符并验证它的零知识证明。

所有者可以通过调用registerValidator方法来添加验证器。只有验证者才能在检查选民的身份后向智能合约发送承诺。

最后一个方法是getOptionCounter,您可以使用它实时查询投票结果。

这是所有。由于zk-merkel-tree,投票合约超级简单,所有的复杂性都隐藏在库后面。

dApp本身是一个vue.js单页应用程序。通过使用zk-merkel-tree中的generatcommit,在VoterRegistration组件中生成承诺和无效符,并存储在本地存储中。

this.commitment=JSON.parse(
  localStorage.getItem("zktree-vote-commitment")
);
if (!this.commitment) {
  this.commitment=await generateCommitment();
  localStorage.setItem(
    "zktree-vote-commitment",
    JSON.stringify(this.commitment)
  );
}

验证器使用ValidatorTool组件将承诺发送到区块链。它从合同中读取合同地址。Json(由部署过程生成),并将具有唯一哈希值的承诺发送给投票合约。

const abi=[
  "function registerCommitment(uint256 _uniqueHash, uint256 _commitment)",
];
const provider=new ethers.providers.Web3Provider(
  (window as any).ethereum
);
await provider.send("eth_requestAccounts", []);
const signer=provider.getSigner();
const contracts=await (await fetch("contracts.json")).json();
const contract=new ethers.Contract(contracts.zktreevote, abi, signer);
try {
  await contract.registerCommitment(this.uniqueHash, this.commitment);
} catch (e) {
  alert(e.reason);
}

投票人使用Vote组件进行投票。它通过使用ZK-merkel-tree中的calculateMerkleRootAndZKProof方法生成ZK证明,并将其发送到带有无效符的区块链。

const commitment=JSON.parse(
  localStorage.getItem("zktree-vote-commitment")
);

const abi=[
  "function vote(uint _option,uint256 _nullifier,uint256 _root, 
   uint[2] memory _proof_a,uint[2][2] memory _proof_b,
   uint[2] memory _proof_c)",
];
const provider=new ethers.providers.Web3Provider(
  (window as any).ethereum
);
await provider.send("eth_requestAccounts", []);
const signer=provider.getSigner();
const contracts=await (await fetch("contracts.json")).json();
const contract=new ethers.Contract(contracts.zktreevote, abi, signer);
const cd=await calculateMerkleRootAndZKProof(
  contracts.zktreevote,
  signer,
  TREE_LEVELS,
  commitment,
  "verifier.zkey"
);
try {
  await contract.vote(
    this.option,
    cd.nullifierHash,
    cd.root,
    cd.proof_a,
    cd.proof_b,
    cd.proof_c
  );
} catch (e) {
  alert(e.reason);
}

正如您所看到的,代码非常简单,因为ZKP的所有复杂性都隐藏在zk-merkel-tree库中。基于此代码,您可以轻松构建自己的投票系统。

可能的改进:

  • 选民白名单。您可以在投票前根据投票者的唯一散列构建Merkle树,并在验证器发送承诺时检查投票者是否存在于其中。它可以防止使用假身份。
  • 更好的验证器管理。如果有数千个验证器,那么Merkle树是批量注册它们的更好方法。如果选民和验证者被划分为地区,那么您可以为地区生成单独的Merkle树,并将验证者分配给这些树。
  • 更好的验证方法。在这个系统中作弊的唯一方法是有人使用假身份进行投票。这就是验证非常重要的原因。有更多的方法可以防止恶意验证器。例如,系统可以为一个选民随机选择2或3个验证者。它们都是恶意的可能性很小。或者它可以记录验证过程,其他验证者随机检查录制的视频。投票作弊是一种犯罪行为,因此验证者恶意的风险非常高。
  • 集成Jitsi Meet等视频会议系统。如果集成了视频会议系统,发送承诺和整个验证过程可以非常容易。选民可以填写一张表格,填写他们的数据,并在确认之前发送出去。验证者只需通过摄像头检查身份证,如果一切正常,他就可以通过点击按钮将承诺和唯一哈希发送到区块链。
  • 数字身份证支持。如果选民拥有可用于对承诺进行数字签名的数字身份证,则可以跳过整个验证过程。这种方法非常便宜,因为不需要人力资源,所以可以经常使用,而且选民可以更多地参与决策。

去中心化是区块链最重要的方面,Web3和基于区块链的匿名投票是它的圣杯。

我希望这篇短文和zk-merkel-tree库可以成为其他人构建自己的系统的一个良好起点。

JavaScript 开发者,我们经常忘记并不是所有人都像我们一样了解 JavaScript,这被称为知识的诅咒:当我们精通某个内容的时候,我们就不记得自己作为新人的时候有多么困惑。我们总是对其他人的能力估计过高,因此我们觉得,自己写的类库需要一些 JavaScript 代码去初始化和配置也很正常。然而,一些用户却在使用过程中大费周折,他们疯狂地从文档中复制粘贴例子并随机组合这些代码,直到它们生效为止。

  • JavaScript(JS)中如何检查一个对象(Object)是否包含指定的键(属性)

你或许会想:“所有写 HTML 和 CSS 的人都会 JavaScript,对吧?”

你错了。来看看我的调查结果吧,这是我所知道的唯一相关数据了。

根据投票结果来看,每两个写 HTML 和 CSS 的人中,就有一个对 JavaScript 没有好感。 这是个值得让人深思的数据结果。

举个例子,以下的代码用来初始化一个 jQuery UI 自动完成库。

toml<div class="ui-widget">
    <label for="tags">Tags: </label>
    <input id="tags">
</div>
toml$( function() {
    var availableTags=[
        "ActionScript",
        "AppleScript",
        "Asp",
        "BASIC",
        "C"
    ];
    $( "#tags" ).autocomplete({
        source: availableTags
    });
} );

你觉得这很简单,甚至觉得即使那些根本不会 JavaScript 的人也会觉得简单,对吧?

错!非程序员在文档中看到这个例子的时候,脑子里会闪过各种问题:“我该把这段代码放哪儿呢?”“这些花括号、冒号和方括号都是什么意思?”“我要用这些吗?”“如果我的元素没有 ID 怎么办?”等等。所以即使这段极其简短的代码也要求人们了解对象字面量、数组、变量、字符串、如何获取 DOM 元素的引用、事件、 DOM 树何时构建完毕等等更多知识。这些对于程序员来说微不足道的事情,对于不会 JavaScript 、只会写 HTML 的人来说都是一场艰难的攻坚战。

  • JavaScript(JS)中怎么遍历数组?一文讲解 JS 遍历数组的方法

现在来看一下 HTML5 中的等效声明性代码:

html<div class="ui-widget">
    <label for="tags">Tags: </label>
    <input id="tags" list="languages">
    <datalist id="languages">
        <option>ActionScript</option>
        <option>AppleScript</option>
        <option>Asp</option>
        <option>BASIC</option>
        <option>C</option>
    </datalist>
    </div>

这不仅让写 HTML 的人看得更清楚更明白,也对程序员来说更为简单。我们看到所有的内容都同时被设置好,不必关心什么时候初始化、如何获取元素的引用以及如何设置每个内容,无需知道哪个函数是用来初始化或者它需要什么参数。在更高级的使用情况中,还会添加一个 JavaScript API 来允许动态创建属性和元素。这遵循了一条最基本的 API 设计原则:让简单的内容变得更简单,让复杂的内容得以简单实现。

  • REST API 设计规范:最佳实践和示例

这给我们上了一堂关于 HTML API 的重要一课:HTML API 不光要给那些了解 JavaScript 但水平有限的人带来福音,还要让我们程序员在普通的工作中也要不惜牺牲程序的灵活性来换取更高的表述性。然而不知道为什么,我们在写自己的类库的时却总忘记这些原则。

那么什么是 HTML API 呢?根据维基百科的定义,API(也就是应用程序接口)是“用于构建应用程序软件的一组子程序定义、协议和工具”。在 HTML API 中,定义和协议就是 HTML ,工具在 HTML 中配置。HTML API 通常由可用于现有 HTML 内容的类和属性模式组成。通过 Web 组件,甚至可以像玩游戏一般自定义元素名称和 Shadow DOM,HTML API 甚至能拥有完整的内部结构,并且对页面其余部分隐藏实现细节。但是这并不是一篇关于 Web 组件的文章,Web 组件给予了 HTML API 设计者更多的能力和选择,但是良好的(HTML)API 设计原则都是可以举一反三的。

HTML API 加强了设计师和工程师之间的合作,减轻工程师肩上的工作负担,还能让设计师创造更具还原度的原型。在类库中引入 HTML API 不仅让社区更具包容性,最终还能造福程序员。

并不是每个类库都需要 HTML API。 HTML API 在使用了 UI 元素的类库中非常有用,比如 galleries、drag-and-drop、accordions、tabs、carousels 等等。经验表明,如果一个非程序员不能理解该类库的功能,它就不需要 HTML API。比如,那些简化代码或者帮助管理代码的库就不需要 HTML API。那 MVC 框架或者 DOM 助手之类的库又怎会需要 HTML API 呢?

目前为止,我们只讨论了 HTML API 的定义、功能和用处,文章剩下的部分是关于如何设计一个好的 HTML API。

初始化选择器

在 JavaScript API 中,初始化是被类库的用户严格控制的:因为他们必须手动调用函数或者创建对象,精确地控制着其运行的时间和基础。在 HTML API 中,我们要帮用户选择,同时也要确保不会妨碍那些仍然使用 JavaScript 的用户,因为他们可能希望得到完全控制。

最常见的兼容两种使用场景的办法是:只有匹配到给定选择器(通常是一个特定的类)时才会自动初始化。Awesomplete 就是采用的这种方法,只选取具有 class="awesomplete" 的 input 元素进行初始化。

有时候,简化自动初始化比做显式选择初始化更重要。当你的类库需要运行在众多元素之上时,避免手动给每个元素单独添加类比显式选择初始化更加重要。比如,Prism 自动高亮任何包含 language-xxx 类的 <code> 元素(HTML5 的说明中建议指定代码段的语言)及其包含languate-xxx 类的元素内部的 <code> 元素。这是因为 Prism 可能会用在一个有着成千上万代码段的博客系统中,回过头去给每一个元素添加类将会是一项非常巨大的工程。

在可以自由地使用 init 选择器的情况下,最好的做法是允许选择是否自动化。比如,Stretchy 默认自动调整每个 <input><select><textarea>的尺寸,但是也允许通过 data-stretchy-filter 属性自定义指定其他元素为 init 选择器。Prism 支持 <script> 元素的 data-manual 属性来完全取消自动初始化。良好的实践应该允许 HTML 和 JavaScript 都能设置这个选项,来适应 HTML 和 JavaScript 两种类库的用户。

最小化初始标记

那么,对于 init 选择器的每个元素,你的类库都需要有一个封包、三个内部的按钮和两个相邻的 div 该怎么办呢?小问题,自己生成就好了。但是这种琐碎的工作更适合机器,而不是人。不要期望每个使用类库的人都同时使用了模板系统:许多人还在使用手动添加标记,他们会发现这样建造系统太过复杂。因此,我们应该让他们更轻松些。

这种做法也最小化了错误风险:如果一个用户仅仅引入了用来初始化的类却没有引入所有需要的标记怎么办?如果不需要添加额外的标记,就不会产生错误。

这条规则中有一个例外:优雅地退化并渐进地增强。比如,即使单个具有 data- * 属性的元素并在 data-* 中添加所有选项就可以实现,在嵌入推文的时候也还是会涉及很多标记。这样做是为了在 JavaScript 加载和运行之前推文就可读。一个良好的经验法则就是扪心自问“即使在没有 JavaScript ,多余的标记能否给终端用户带来好处?”如果是,那么就引入;如果不是,那就要用类库生成。

便于用户使用还是让用户自定义也是一组经典的矛盾:自动生成所有的标记会易于用户使用,让用户自定义又显得更加灵活。在你需要的时候,灵活性如雪中送炭,在不需要的时候却适得其反,因为你不得不手动设置所有的参数。为了平衡这两种需要,你可以生成那些需要但不存在的标记。比如,假设你需要给所有的 .foo 元素外层添加一个 .foo-container 元素。首先,通过element.closest(".foo-container") 检查 .foo 元素的父元素或者任何的祖先元素(这样最好了)是否含有 foo-container 类,如果有的话,你就不用生成新的元素,直接使用就可以了。

设置

通常,设置应该通过在恰当的元素上使用 data-* 属性来实现。如果你的类库中添加了成千上万的属性,然后你希望给它们添加命名空间来避免和其他类库混淆,比如这样 data-foo-*(foo 是基于类库名字的一到三个字母长度的前缀)。如果名字显得太长,你可以使用 foo-*,但你要知道,这种方式会打破 HTML 验证并且会使得一些勤奋的 HTML 作者因此而弃用你的类库。理想情况下,只要代码不会太臃肿,以上两种情况都应该支持。目前还没有完美的解决办法,因此在 WHATWG 中展开了一场如火如荼的讨论:是否应该让自定义的属性前缀合法化。

尽可能地遵从 HTML 的惯例。比如,你使用了一个属性来做布尔类型的设置,当该属性出现时无论其值如何都被视为 true,若不出现则被视为 false,不要期望可以用 data-foo="true" 或者 data-foo="false" 来代替。

你也可以使用类进行布尔值设置。一般情况下它的语法和布尔属性类似:类存在的时候是 true 不出现的时候就是 false。如果你想反过来设置,那就用一个 no- 前缀(比如,no-line-number)。但是要记住,类名可不像属性一样只有 data-*,因此这种方式很可能会和用户现存的类名冲突,因此你可以考虑一下在类名中使用 foo- 这样的前缀来避免冲突。但也有可能在后期的维护中发现这些类并未被 CSS 使用所以误删,这又是另一个隐患。

当你需要设置一组相关的布尔值时,使用空格区分会比使用多个分隔符的方式好很多。

html<!-- 第一种-->
<div data-permissions="read add edit delete save logout"> 
html<!-- 第二种-->
<div data-read data-add data-edit data-delete data-save data-logout">
html <!-- 第三种-->
 <div class="read add edit delete save logout">

比如,第一种当时就比后面两种好得多,因为后者可能会造成很多的冲突。你还可以使用 ~= 属性选择器来定位单个元素,比如 element.matches("[data-permissions~=read]") 可以检查该元素是否有 read 权限。

如果设置内容的类型是数组(array)或者对象(object) ,那么你就可以使用 data-* 属性来关联到另一个元素。比如, HTML5 中的自动完成:因为自动完成需要一个建议列表,你可以使用 data-* 属性并通过 ID 联系到包含建议内容的 <datalist> 元素。

HTML 有一个惯例很让人头痛:在 HTML 中,用属性联系到另一个元素通常是靠引用其 ID 实现的(试想一下 <label for="...">)。然而,这种方法相当受限制:如果能够允许使用选择器或者甚至允许嵌套将更为方便,其效果将会极大地依赖于你的使用情况。要记住,稳定性重要,但实用性更加重要。

即使有些设置内容不能在 HTML 中指定也没关系。在 JavaScript 中以函数为设置值的部分被称作“高级自定义”。试想一下 Awesomplete:所有数字、布尔值、字符串和对象都可以通过 data-* 属性(listminCharsmaxItemsautoFirst)设置,所有的函数设置只能通过 JavaScript 使用(filtersortitemreplacedata),这样会写 JavaScript 函数来配置类库的人就可以使用 JavaScript API 了。

正则表达式(regex)处在灰色地带:通常只有程序员才知道正则表达式(甚至程序员在使用的时候也会有问题!);那么,乍看之下,在 HTML API 中引入正则表达式类型的设置并没有意义。然而,HTML5 确实引入了这样的设置(<input pattern="regex">),并且我觉得很成功,因为非程序员能在正则词典中找到他们的用例并复制粘贴。

继承

如果你的 UI 库在每个页面只会调用一两次,继承可能不是很重要。然而,如果要应用于多个元素,通过类或者属性给每个元素做相同的配置将会非常令人头疼。咱要记住并不是每个人都用了构建系统,尤其是非程序员。在这些情况下,定义能够从祖先元素继承设置将会变得非常有用,那样多个实例就可以被批量设置了。

还拿 Smashing Magazine 中使用的时下流行的语法高亮类库 —— Prism 来举例。高亮语句是通过 language-xxx 形式的类来配置的。如你所见,这违反了我们在前文中谈过的规则,但这只是一种主观决策,因为 HTML5 手册中建议如此。在有许多代码段的页面上(想象一下,在博客文章中使用内联 <code> 元素的频率!),在每个 <code> 元素中指定代码语句将会非常烦人。为了减轻这种痛苦,Prism 支持继承这些类:如果一个 <code> 元素自己没有 language-xxx 类,那么将会使用其最近的祖先元素的 language-xxx 类。这使得用户可以设置全局的代码语句(通过在 <body> 或者 <html> 元素上设置类)或者设置区块的代码语句,并且可以在拥有不同语句的元素或者区块上重写设置。

现在 CSS 变量已经被所有的浏览器支持,它们可以用于以下设置:他们默认可以被继承,并且可以以内联的方式通过 style 属性设置,也可以通过 CSS 或者 JavaScript 设置。在代码中,你可以通过getComputedStyle(element).getPropertyValue("--variablename") 获取它们。除了浏览器支持,其主要的劣势就是开发者们还没习惯使用它们,但是那已经发生改变了。并且,你不能像监视元素和属性的一般通过 MutationObserver 来监视其改变。

全局设置

大多数 UI 类库都有两组设置:定义每个组件表现形式的设置和定义整个类库表现形式的全局设置。目前为止,我们主要讨论了前者,你现在可能好奇全局设置该在设置在哪里。

进行全局设置的一个好地方就引入类库的 <script> 元素。你可以通过 document.currentScript 获取该元素,这有着非常好的浏览器支持。好处就是,这对于设置的作用域非常清楚,因此它们的名字可以起的更短(比如 data-filter 而不是 data-stretchy-filter)。

然而,你不能只在 <script> 元素中进行设置,因为一些用户可能会在 CMS 中使用你的类库,而 CMS 中不允许用户自定义 <script> 元素。你也可以在 <html><body> 元素或者甚至任何地方设置,只要你清楚地声明了属性值重复的时候哪个会生效。

文档

那么,你已经掌握了如何在类库中设置一个漂亮的声明性的 API,那自然很好,然而,如果你所有的文档都写得只有会 JavaScript 的用户才看得懂,那么就只有很少人能使用了。我记得曾经看过一个很酷的类库,基于 URL 并通过切换元素的 HTML 属性来切换元素的表现形式。然而,这漂亮的 HTML API 并不能被其目标人群所使用,因为整篇文档中都充满了 JavaScript 引用。最开始的例子开头就是“这和 location.href.match(/foo/)等价”。非程序员哪能看懂这个呀?

同时要记得许多人并不会任何编程语言而不仅仅是 JavaScript。你希望用户能够读懂并理解的文中的模型、视图、控制器或者其他软件工程观念,但结果无非是让他们感到迷惑。

当然,你应该在文档中写 API 里 JavaScript 的内容,你可以写在“高级使用”部分。然而,如果你在文档一开头就引用 JavaScript 对象和函数或者软件工程的观念,那么你实质上就是在告诉非程序员这个类库不是给他们用的,因此你就排除了一大批潜在用户。不幸的是,大部分的 HTML API 类库文档都受这些问题困扰着,因为 HTML API 经常被视为是程序员的捷径,而并不是给非程序员使用的。庆幸的是,这种状况在未来可以有改变。

Web 组件

在不远的未来,Web 组件百分之百将会彻底改变 HTML API。<template> 元素将会允许作者提供惰性加载的脚本。自定义元素将使得用户可以像原生的 HTML 一样使用更多优雅的 init 标记。引入 HTML 也将使得作者能够仅引入一个文件来替代三个样式表、五个脚本和十个模板(如果浏览器能够同时获取并且不再认为 ES6 模块是一种竞争技术)。Shadow DOM 使得类库可以将复杂的 DOM 结构适当压缩并且不会影响用户自己的标记。

然而除了 <template>,浏览器对其他三个特征的支持目前受限。因此他们需要更高的聚合度,以此来减少对类库的影响。这将会是我们在未来一段时间里需要不断关注的东西。

  • 源于:https://www.smashingmagazine.com/2017/02/designing-html-apis/



主介绍:?在职Java研发工程师、专注于程序设计、源码分享、技术交流、专注于Java技术领域和毕业设计?


项目名称


SSM框架基于JavaWeb在线投票系统的设计与实现源码


视频效果


SSM框架基于JavaWeb在线投票系统的设计与实现源码



SSM框架基于JavaWeb在线投票系统的设计与实现源码




系统说明


根据系统分析,将在线投票管理系统分为前台功能模块和后台功能模块。其中系统前台功能实现用户注册、用户登录、浏览、投票、投票中心和投票历史等功能。系统前台功能如图5-2所示:


?编辑


图5-2 系统前台功能模块结构图


用户注册:用户填写用户名、密码和性别,点击注册按钮进行注册。


用户登录:用户填写已经注册的用户名和密码,点击登录按钮进行登录。


浏览:用户可以浏览在线投票管理系统中公共开放内容。


投票:用户选择自己需要的投票,针对主题,勾选选项,进行投票操作。投票方式支持单选只能投一次、单选一天只能投一次、多选只能投一次、多选一天只能投一次等四种方式。


投票中心:在线投票管理系统展示所有投票主题供用户选择。


投票历史:存储用户已经投票的历史内容,用户登陆后方可查看。


系统后台功能实现以下功能,投票信息管理、详细投票查看、用户信息管理、投票信息统计和管理员登录等功能。系统后台功能如图5-3所示:


?编辑


图5-3 系统后台功能模块结构图


投票信息管理:管理员进行投票信息管理,可以管理投票主题和投票选项。针对投票主题,可以添加投票主题(需要填写主题名称、主题类型、开始时间、结束时间和主题简介)、查看主题、修改主题和删除主题;针对投票选项管理,可以添加选项(需要填写选项名称和选择所属主题)、查看选项、修改选项和删除选项。


详细投票查看:管理员查看投票的详细信息,以列表形式显示,每条投票详细信息包括:投票ID、用户名、投票主题、投票选项、总投票数和投票时间等信息。


用户信息管理:管理员进行用户信息管理,可以添加用户(需要填写用户名、密码、性别和状态)、查看用户信息、修改用户信息和删除用户。


投票信息统计:管理员进行投票信息统计,默认显示所有的投票主题统计,输入搜索主题名称后显示单个主题的投票统计。


管理员登录:管理员输入用户名、密码和验证码,点击登录按钮,进行登录操作。


环境需要


1.运行环境:最好是java jdk 1.8,我们在这个平台上运行的。其他版本理论上也可以。
2.IDE环境:IDEA,Eclipse,Myeclipse都可以。推荐IDEA;
3.tomcat环境:Tomcat 7.x,8.x,9.x版本均可
4.硬件环境:windows 7/8/10 1G内存以上;或者 Mac OS;
5.数据库:MySql 5.7版本;
6.是否Maven项目:否;


技术栈


1. 后端:Spring+SpringMVC+Mybatis
2. 前端:JSP+CSS+JavaScript+jQuery


使用说明


1. 使用Navicat或者其它工具,在mysql中创建对应名称的数据库,并导入项目的sql文件;
2. 使用IDEA/Eclipse/MyEclipse导入项目,Eclipse/MyEclipse导入时,若为maven项目请选择maven;
若为maven项目,导入成功后请执行maven clean;maven install命令,然后运行;
3. 将项目中springmvc-servlet.xml配置文件中的数据库配置改为自己的配置;
4. 运行项目,在浏览器中输入http://localhost:8080/ 登录


运行截图


?编辑

?编辑

?编辑

?编辑

?编辑

?编辑

?编辑

?编辑

?编辑

?编辑


用户管理控制层:


package com.houserss.controller;

import javax.servlet.http.HttpSession;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.houserss.common.Const;
import com.houserss.common.Const.Role;
import com.houserss.common.ServerResponse;
import com.houserss.pojo.User;
import com.houserss.service.IUserService;
import com.houserss.service.impl.UserServiceImpl;
import com.houserss.util.MD5Util;
import com.houserss.util.TimeUtils;
import com.houserss.vo.DeleteHouseVo;
import com.houserss.vo.PageInfoVo;

/**
 * Created by admin
 */
@Controller
@RequestMapping("/user/")
public class UserController {
    @Autowired
    private IUserService iUserService;

    /**
     * 用户登录
     * @param username
     * @param password
     * @param session
     * @return
     */
    @RequestMapping(value="login.do",method=RequestMethod.POST)
    @ResponseBody
    public ServerResponse<User> login(User user,String uvcode, HttpSession session){
        String code=(String)session.getAttribute("validationCode");
        if(StringUtils.isNotBlank(code)) {
            if(!code.equalsIgnoreCase(uvcode)) {
                return ServerResponse.createByErrorMessage("验证码不正确");
            }
        }
        ServerResponse<User> response=iUserService.login(user.getUsername(),user.getPassword());
        if(response.isSuccess()){
            session.setAttribute(Const.CURRENT_USER,response.getData());
        }
        return response;
    }

  
    
    
}



管理员管理控制层:



package com.sxl.controller.admin;

import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import com.sxl.controller.MyController;

@Controller("adminController")
@RequestMapping(value="/admin")
public class AdminController extends MyController {
	

	@RequestMapping(value="/index")
	public String frame(Model model, HttpServletRequest request)throws Exception {
		return "/admin/index";
	}
	
	@RequestMapping(value="/main")
	public String main(Model model, HttpServletRequest request)throws Exception {
		return "/admin/main";
	}
	
	@RequestMapping(value="/tj1")
	public String tj1(Model model, HttpServletRequest request)throws Exception {
		String sql="select DATE_FORMAT(insertDate,'%Y-%m-%d') dates,sum(allPrice) price from t_order order by DATE_FORMAT(insertDate,'%Y-%m-%d')  desc";
		List<Map> list=db.queryForList(sql);
		model.addAttribute("list", list);
		System.out.println(list);
		return "/admin/tj/tj1";
	}
	
	
	@RequestMapping(value="/password")
	public String password(Model model, HttpServletRequest request)throws Exception {
		return "/admin/password";
	}
	
	
	@RequestMapping(value="/changePassword")
	public ResponseEntity<String> loginSave(Model model,HttpServletRequest request,String oldPassword,String newPassword) throws Exception {
		Map admin=getAdmin(request);
		if(oldPassword.equals(admin.get("password").toString())){
			String sql="update t_admin set password=? where id=?";
			db.update(sql, new Object[]{newPassword,admin.get("id")});
			return renderData(true,"1",null);
		}else{
			return renderData(false,"1",null);
		}
	}
}



修改密码业务逻辑:



package com.sxl.controller.admin;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import com.sxl.controller.MyController;

@Controller("userController")
@RequestMapping(value="/user")
public class UserController extends MyController {
	

	@RequestMapping(value="/index")
	public String frame(Model model, HttpServletRequest request)throws Exception {
		return "/user/index";
	}
	
	@RequestMapping(value="/main")
	public String main(Model model, HttpServletRequest request)throws Exception {
		return "/user/main";
	}
	
	
	@RequestMapping(value="/password")
	public String password(Model model, HttpServletRequest request)throws Exception {
		return "/user/password";
	}
	
	
	@RequestMapping(value="/changePassword")
	public ResponseEntity<String> loginSave(Model model,HttpServletRequest request,String oldPassword,String newPassword) throws Exception {
		Map user=getUser(request);
		if(oldPassword.equals(user.get("password").toString())){
			String sql="update t_user set password=? where id=?";
			db.update(sql, new Object[]{newPassword,user.get("id")});
			return renderData(true,"1",null);
		}else{
			return renderData(false,"1",null);
		}
	}
	@RequestMapping(value="/mine")
	public String mine(Model model, HttpServletRequest request)throws Exception {
Map user=getUser(request);Map map=db.queryForMap("select * from t_user where id=?",new Object[]{user.get("id")});model.addAttribute("map", map);		return "/user/mine";
	}
	
	

	@RequestMapping(value="/mineSave")
	public ResponseEntity<String> mineSave(Model model,HttpServletRequest request,Long id
		,String username,String password,String name,String gh,String mobile) throws Exception{
		int result=0;
			String sql="update t_user set name=?,gh=?,mobile=? where id=?";
			result=db.update(sql, new Object[]{name,gh,mobile,id});
		if(result==1){
			return renderData(true,"操作成功",null);
		}else{
			return renderData(false,"操作失败",null);
		}
	}
	}



通用管理模块:


package com.sxl.controller;


import java.nio.charset.Charset;
import java.util.Locale;
import java.util.ResourceBundle;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;

import com.sxl.util.JacksonJsonUtil;
import com.sxl.util.StringUtil;
import com.sxl.util.SystemProperties;


public class BaseController {
	public static final Long EXPIRES_IN=1000 * 3600 * 24 * 1L;// 1天

	@Autowired
	private SystemProperties systemProperties;

	/**
	 * 获得配置文件内容
	 */
	public String getConfig(String key) {
		return systemProperties.getProperties(key);
	}

	/**
	 * 返回服务器地址 like http://192.168.1.1:8441/UUBean/
	 */
	public String getHostUrl(HttpServletRequest request) {
		String hostName=request.getServerName();
		Integer hostPort=request.getServerPort();
		String path=request.getContextPath();

		if (hostPort==80) {
			return "http://" + hostName + path + "/";
		} else {
			return "http://" + hostName + ":" + hostPort + path + "/";
		}
	}

	/***
	 * 获取当前的website路径 String
	 */
	public static String getWebSite(HttpServletRequest request) {
		String returnUrl=request.getScheme() + "://"
				+ request.getServerName();

		if (request.getServerPort() !=80) {
			returnUrl +=":" + request.getServerPort();
		}

		returnUrl +=request.getContextPath();

		return returnUrl;
	}



	/**
	 * 初始化HTTP头.
	 * 
	 * @return HttpHeaders
	 */
	public HttpHeaders initHttpHeaders() {
		HttpHeaders headers=new HttpHeaders();
		MediaType mediaType=new MediaType("text", "html",
				Charset.forName("utf-8"));
		headers.setContentType(mediaType);
		return headers;
	}

	/**
	 * 返回 信息数据
	 * 
	 * @param status
	 * @param msg
	 * @return
	 */
	public ResponseEntity<String> renderMsg(Boolean status, String msg) {
		if (StringUtils.isEmpty(msg)) {
			msg="";
		}
		String str="{\"status\":\"" + status + "\",\"msg\":\"" + msg + "\"}";
		ResponseEntity<String> responseEntity=new ResponseEntity<String>(str,
				initHttpHeaders(), HttpStatus.OK);
		return responseEntity;
	}

	/**
	 * 返回obj数据
	 * 
	 * @param status
	 * @param msg
	 * @param obj
	 * @return
	 */
	public ResponseEntity<String> renderData(Boolean status, String msg,
			Object obj) {
		if (StringUtils.isEmpty(msg)) {
			msg="";
		}
		StringBuffer sb=new StringBuffer();
		sb.append("{");
		sb.append("\"status\":\"" + status + "\",\"msg\":\"" + msg + "\",");
		sb.append("\"data\":" + JacksonJsonUtil.toJson(obj) + "");
		sb.append("}");

		ResponseEntity<String> responseEntity=new ResponseEntity<String>(
				sb.toString(), initHttpHeaders(), HttpStatus.OK);
		return responseEntity;
	}


	/***
	 * 获取IP(如果是多级代理,则得到的是一串IP值)
	 */
	public static String getIpAddr(HttpServletRequest request) {
		String ip=request.getHeader("x-forwarded-for");
		if (ip==null || ip.length()==0 || "unknown".equalsIgnoreCase(ip)) {
			ip=request.getHeader("Proxy-Client-IP");
		}

		if (ip==null || ip.length()==0 || "unknown".equalsIgnoreCase(ip)) {
			ip=request.getHeader("WL-Proxy-Client-IP");
		}

		if (ip==null || ip.length()==0 || "unknown".equalsIgnoreCase(ip)) {
			ip=request.getRemoteAddr();
		}

		if (ip !=null && ip.length() > 0) {
			String[] ips=ip.split(",");
			for (int i=0; i < ips.length; i++) {
				if (!"unknown".equalsIgnoreCase(ips[i])) {
					ip=ips[i];
					break;
				}
			}
		}

		return ip;
	}

	/**
	 * 国际化获得语言内容
	 * 
	 * @param key
	 *            语言key
	 * @param args
	 * @param argsSplit
	 * @param defaultMessage
	 * @param locale
	 * @return
	 */
	public static String getLanguage(String key, String args, String argsSplit,
			String defaultMessage, String locale) {
		String language="zh";
		String contry="cn";
		String returnValue=defaultMessage;

		if (!StringUtil.isEmpty(locale)) {
			try {
				String[] localeArray=locale.split("_");
				language=localeArray[0];
				contry=localeArray[1];
			} catch (Exception e) {
			}
		}

		try {
			ResourceBundle resource=ResourceBundle.getBundle("lang.resource",
					new Locale(language, contry));
			returnValue=resource.getString(key);
			if (!StringUtil.isEmpty(args)) {
				String[] argsArray=args.split(argsSplit);
				for (int i=0; i < argsArray.length; i++) {
					returnValue=returnValue.replace("{" + i + "}",
							argsArray[i]);
				}
			}
		} catch (Exception e) {
		}

		return returnValue;
	}
}


?