iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 13
1
Blockchain

區塊練起來-智能合約與DApp開發系列 第 13

[區塊練起來-智能合約與DApp開發] DAY 13 - 實戰智能合約!區塊鏈履歷(1)

  • 分享至 

  • xImage
  •  

貼心小語

前面章節已經將 Solidity 基本知識交代的差不多了,終於可以進入實戰演練了!我們來寫個小專題來練習智能合約開發以及為 DApp 開發做準備!/images/emoticon/emoticon18.gif


小專題目標

我們將使用智能合約的方式來提供高可信度的個人履歷表,讓履歷的真實性更高!

情境

  • 假設每個公民都有一份屬於自己的智能合約,這份智能合約就是你個人的履歷表
  • 該智能合約是由政府單位部署
  • 求職者只有基本的編輯權限,如:個人資料、專業技術、自傳等
  • 企業單位在雇用員工時,應在該員工的智能合約中添加經歷等資訊
  • 教育單位應在該求職者的智能合約中添加學歷、修課證明等

參與者

  • 政府單位 :合約發起者,為 owner ,可以設定:企業與教育單位編輯合約的權限、建立求職者的基本資料
  • 企業單位 :透過政府單位給予編輯權限來設定:員工於該公司的職稱、工作期間
  • 教育單位 :透過政府單位給予編輯權限來設定:學生的學歷、修課證明、專業證照
  • 求職者 :可以填寫:自傳、專業技術、基本聯絡方式

合約架構

  • Resume.sol :履歷表合約,提供操作履歷表合約的各式 function ,有 Get 、 Set 以及 Remove 的操作
  • ResumeBase.sol :履歷表合約的父合約,定義狀態變數、列舉(enum)、結構(struct)、修飾詞(modifier)、事件(event)以及內部用函式
  • StrLib.sol :字串用函式庫,針對字串操作所設計的函式庫

父合約 - ResumeBase

我們先把合約書拿出來!對,就是先寫contractXD

pragma solidity ^0.5.0;

contract ResumeBase {
  constructor() public {}
}

再來要思考這個履歷智能合約應該要有哪些狀態變數,以及定義結構(struct)。我們知道 政府單位 會是合約的發起人,表示我們可以宣告一個狀態變數來存放政府單位的帳戶:

address internal government;

constructor() public {
  government = msg.sender;
}

我們只要是不會被外部調用的函式或狀態變數,一律用 internalprivate

履歷一定會有這個求職者的相關資訊,有部分基本資料是由政府單位所管,畢竟都由政府來部署了,總會知道這個求職者的相關資訊吧!大致上就是:姓名、帳戶(Account)、年紀、性別。那除了這些,有一部份的資料是由求職者所管,就是:專業技術、自傳、聯絡方式。所以我們就可以定義一個結構來統整,並使用狀態變數來儲存:

Profile public profile;

struct Profile {
  string name;
  address account;
  uint8 age;
  Gender gender;
  string contact;
  string autobiography;
}

enum Gender {
  male,
  female,
  other
}

不曉得大家有沒有發現到把 profile 設為 public ,原因是我們要讓外部能夠取得個人資訊,透過這樣的方法, Solidity 會 自動 將該變數建立一個 Getter 函式,讓外部可以透過這個函式來取得個人資訊,實在是很貼心呢!

既然政府都要 key 入求職者的資料了,乾脆就在部署的時候就給定吧!

constructor(string memory name, address account, uint8 age, Gender gender) public {
  government = msg.sender;
  profile = Profile({
    name: name,
    account: account,
    age: age,
    gender: gender,
    contact: "",
    autobiography: ""
  });
}

我們知道這個履歷會有 教育單位企業單位 來寫入學經歷等資訊,但只要是已經修畢學業或是從某個公司離職了,就不應該讓這些本來被賦予修改權限的機構繼續保有修改權,所以這裡要定義一個機構的結構,並宣告一個為 mapping 的狀態變數來儲存,內含的資訊應該要有:名稱、機構單位(企業或教育)、帳戶(Account)以及權限:

mapping(address => Organization) internal organizations;

struct Organization {
  string name;
  OrganizationType property;
  address account;
  bool permission;
}

enum OrganizationType {
  everyOne,
  school,
  company
}

剛剛提到了學經歷,就表示一定有個地方來儲存求職者的學經歷,這邊我們就設定兩個狀態變數,分別儲存學歷與經歷並分別定義結構,需要特別注意的是學歷與經歷通常不會只有單一一個,所以肯定是要用陣列的方式儲存的:

Education[] internal educations;
Job[] internal experiences;

struct Job {
  Organization company; //公司資訊
  string position; //職位
  uint startDate; //就職日
  uint endDate; //離職日
}

struct Education {
  Organization school; //學校資訊
  EducationStatus status; //學業狀態
  string major; //主修
  Course[] courses; //修課證明
  License[] licenses; //證照
}

struct Course {
  string name;
  string content; //課程內容
  string comment; //老師評語
  uint8 grade; //成績
}

struct License {
  string name;
  string content; //檢定內容
}

enum EducationStatus {
  undergraduate, //肄業
  learning, //在學中
  graduate //畢業
}

還有專業技術的部分:

Skill[] internal skills;

struct Skill {
  string class; //專業類別
  string name;
}

這樣看下來不知道有沒有人發現,為什麼同樣是要讓外部取得的資訊, skillseducations 以及 experiences 都使用 internal ,原因是 Solidity 在預設情況下是 無法回傳陣列結構的

我們已經把履歷該有的資訊通通都定義好了,但我們還沒有透過 modifier 來做權限管理:

modifier onlyGov {
  require(msg.sender == government, "Permission denied. Please use government account.");
  _;
}

modifier onlySchool {
  bool isSchool = organizations[msg.sender].property == OrganizationType.school;
  require(isSchool && organizations[msg.sender].permission, "Permission denied. Please use school account.");
  _;
}

modifier onlyCompany {
  bool isCompany = organizations[msg.sender].property == OrganizationType.company;
  require(isCompany && organizations[msg.sender].permission, "Permission denied. Please use company account.");
  _;
}

modifier onlyHost {
  require(msg.sender == profile.account, "Permission denied. Please use host account.");
  _;
}

另外我們會有要從陣列中取得資訊的需求,所以要設置一個 modifier 來驗證給定的 index 是不是超出了陣列的範圍,傳入的參數為要檢查的 index 以及陣列的總長度:

modifier IndexValidator(uint index, uint max) {
    require(index < max, "Out of range.");
    _;
}

有些應用場景必須等到交易處理完後才能進行下一步,也就是要讓外部能夠順利了解調用函式之後交易是否處理完畢或是取得此操作的相關資訊,所以這時候我們可以運用之前所學的一項功能來幫助我們達到這樣的效果,答案就是 event ,因為 log 紀錄是在寫收據時才會記錄的,表示外部調用後,可以監聽是否寫入 log 來確定交易進度。這邊我們就順便定義未來會使用到的函式,然後使用這些函式時帶上對應的 status 到 log 中:

event done(DoneCode eventCode, string message);

enum DoneCode {
  setPermission, //設置機構權限
  setEducation, //設置學歷
  setLicense, //設置證照
  setCourse, //設置修課證明
  setExperience, //設置工作經歷
  setJobEndDate, //設置離職日
  setAutobiography, //設置自傳
  setSkill, //設置專業技術
  setContact, //設置聯絡方式
  removePermission, //移除機構權限
  removeSkill //移除專業技術
}

基本上我們這次的 DApp 不會運用到監聽 event ,但還是將這個功能寫進來,一方面是學習如何運用 event ,一方面是讓每一項操作都留下一個證據來表示那筆交易的操作目的。

另外我們在 Resume 合約中,會有需要查詢教育單位、企業單位在陣列中哪個位置,所以需要做一個查詢的函式,這個函式還會使用之前所學的字串比對函式庫,後面會再補上函式庫的程式碼:

using StrLib for string;

function findOrganization(address org, string memory property) internal view returns(int) {
  int index = -1;
  if (property.compare("education")) {
    for (uint i = 0; i < educations.length; i++) {
      if (educations[i].school.account == org) {
        index = int(i);
        break;
      }
    }
  } else if (property.compare("experience")) {
    for (uint i = 0; i < experiences.length; i++) {
      if (experiences[i].company.account == org) {
        index = int(i);
        break;
      }
    }
  }
  return index;
}

StrLib 函式庫

字串處理的函式庫,記得使用要 import 呦:

pragma solidity ^0.5.0;

library StrLib {
  function compare(string memory str1, string memory str2) public pure returns(bool) {
    bytes memory str1_bytes = bytes(str1);
    bytes memory str2_bytes = bytes(str2);
    return keccak256(str1_bytes) == keccak256(str2_bytes);
  }
}

今日小結

把我們小專題的目標定義清楚,並開始撰寫 ResumeBase 合約,這份合約已經能夠滿足 Resume 合約的需求,下一篇將會開始寫 Resume 合約!


上一篇
[區塊練起來-智能合約與DApp開發] DAY 12 - Solidity 特殊變數
下一篇
[區塊練起來-智能合約與DApp開發] DAY 14 - 實戰智能合約!區塊鏈履歷(2)
系列文
區塊練起來-智能合約與DApp開發31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言