政務通——區塊鏈助力政府辦公
1.專案簡介
? 區塊鏈具有不可篡改性以及可追溯性,因此對于一些重要資訊區塊鏈更能夠保障資訊的安全,基于區塊鏈的這兩大特點,本篇將介紹如何將區塊鏈應用于政府辦公,實作協同辦公,資料脫敏上鏈,以及資料溯源打破資料孤島等功能,以小程式為載體,體現區塊鏈在實際生活中的具體作用,總體設計分為四個模塊,具體如表1.1所示,
? 表1.1 功能模塊分析
| 功能模塊 | 技術特點 |
|---|---|
| 1. 用戶管理模塊 | 注冊時候對用戶資訊進行資產數字化處理,用戶密碼等關鍵資訊脫敏上鏈,存盤的是通過sha256運算后的哈希值,保障了用戶的安全,用戶登錄時,輸入密碼進行一次哈希運算,與鏈上比對,即完成“確權”,驗證一致才可登陸, |
| 2. 建言獻策模塊 | 用戶留言內容記錄上鏈,同時對留言內容呼叫外部api,如果留言內容涉及敏感詞,則扣除用戶信用積分,打造一個好的社會信用生態 |
| 3巡檢模塊 | 用戶打卡記錄上鏈,涉及“資料溯源” |
| 4.政務合作模塊 | 體現聯盟鏈的**“多方協作”**特點. |
2 專案優勢
區塊鏈技術的去中心化、不可篡改、可信任、可溯源等特點,使得區塊鏈技術不僅在資料安全領域有所作為,在政務服務系統中也可大展身手,通過對大量資料資訊 的分析和快速處理,區塊鏈應用開發技術可以迅速將有效資訊傳遞至各部門,為扁平 化管理創造了條件,因此本專案有如下特點與優勢,
1、區塊鏈技術可在政府部門間構建起分布式對等網路,讓政府組織結構的資訊傳 遞更加直接高效,部門間可運用區塊鏈技術直接進行點對點資訊傳遞,
2、區塊鏈分布式的模式特點可以實作多部門間的資料同享,可使得政務管理層級 減少,部門與部門間、上級與下級間的溝通會更順暢,對于人員的需求也會相應減少, 政府部門可利用區塊鏈技術打造高效的行政系統,推動政府治理和公共服務模式創新,
3、每個在區塊鏈上獲取資料的主體是平等關系,需要共同承擔管理責任,資料的 變動和更改會同步在整個網路節點上更新,這種變動需要每個參與者確認,即使部分 資料庫系統出現失靈或錯誤,其他節點資料依然完整,資料庫系統依舊可以正常有效 運轉,
4、公共服務部門利用區塊鏈技術可以降低成本、保證資料安全、增加信任、透明 度和可靠性,區塊鏈的特性使得資料可以追根溯源,資料安全性提升且不能隨意變動, 有助于建立權威資料庫,進而建立更安全、開放、包容高效的公共服務平臺,
3.系統實作
? 區塊鏈部分基于FISCO BCOS 開發,FISCO BCOS 是由國內企業主導研發、對外開源、安全可控的企業級金融聯盟鏈底層平臺,另外通過微信小程式作為媒介,客戶端由小程式和后臺管理網站通過https 請求,經過 nginx 進行負載均衡,后臺采用 django,將用戶的 access_token等存盤在redis 快取服務器中,進行定時重繪, 前端采用小程式的原生框架,采用 WXML + WXSS + JS 進行原生開發與布局,
3.1用戶管理模塊
? 該模塊包含用戶注冊登陸以及管理員對用戶信用積分的管理,登錄功能是可確權登錄的操作手段,以此實作用戶的操作安全性,確保用戶的賬 號資料安全為用戶本人操作,用戶在正確登錄小程式后,后臺會獲取登錄用戶的身份, 根據身份給予該用戶不同的權限進行操作,
3.1.1 合約代碼
**1.功能說明:**本合約實作功能主要為:1.用戶注冊2.用戶登錄3.查看用戶資訊
activateUser(string memory _userid,string memory _username,string memory _userpassword, string memory _usertype):用戶實作注冊,傳入用戶的ID號、名字、密碼,用戶身份類別,Login(string memory _userid,string memory _userpassword)用戶的ID、用戶的密碼getUserRecordArray(string userid)用戶的ID
pragma solidity ^0.4.25;
pragma experimental ABIEncoderV2;
import "../lib/SafeMath.sol";
import "../utils/TimeUtil.sol";
import "../utils/StringUtil.sol";
import "../utils/TypeConvertUtil.sol";
import "./TableDefTools.sol";
contract UserControl is TableDefTools{
/******* 引入庫 *******/
using TimeUtil for *;
using SafeMath for *;
using TypeConvertUtil for *;
using StringUtil for *;
/*
* 建構式,初始化使用到的表結構
*
* @param 無
*
* @return 無
*/
constructor() public{
//初始化需要用到的表,用戶資訊表
initTableStruct(t_user_struct, TABLE_USER_NAME, TABLE_USER_PRIMARYKEY, TABLE_USER_FIELDS);
}
// 事件
event REGISTER_USER_EVENT(string userid,string usertype,string activatetime); //注冊用戶事件.記錄注冊人身份,型別,注冊時間
event DEL_CREDITPOINT_EVENT(string user_id,string grade,string time); //扣分時間,扣分人id、扣分數、扣分時間
event ADD_CREDITPOINT_EVENT(string user_id,string grade,string time);
/*
* 1.用戶注冊
*
* @param _userid 用戶id
* @param _fields 用戶資訊表各欄位值拼接成的字串(除最后三個欄位;用逗號分隔,最后三個欄位分別是注冊時間【根據注冊時間生成】,注冊狀態
注冊后這個值默認為1,信用積分默認為100分),包括如下:
* 用戶ID(主鍵)
* 用戶名
* 登陸密碼
* 注冊時間
* 注冊狀態[1代表已注冊,0代表為注冊]
* 信用積分
*
* @return 執行狀態碼
*
* 測驗舉例 引數一:"191867345212322"
* 引數二:"江會文","123456","個人"
*注冊成功回傳SUCCESS否則回傳錯誤碼,錯誤碼對應的問題請參考DB
*/
function activateUser(string memory _userid,string memory _username,string memory _userpassword, string memory _usertype) public returns(int8){
// 獲得當前的時間
string memory _passwordhash=TypeConvertUtil.bytes32ToString(sha256(abi.encode(_userid,_userpassword)));
string memory nowDate = TimeUtil.getNowDate();
string memory firstFiveParams=StringUtil.strConcat7(_username,',',_passwordhash,',',nowDate,',',_usertype);
string memory lastTwoParams = "1,100";
string memory storeFields = StringUtil.strConcat3(firstFiveParams,',',lastTwoParams);
emit REGISTER_USER_EVENT(StringUtil.strConcat2("注冊人的ID為:",_userid),StringUtil.strConcat2("注冊人身份為:",_usertype),StringUtil.strConcat2("注冊時間為:",nowDate));
return insertOneRecord(t_user_struct,_userid,storeFields,false);//最后的false代表主鍵下記錄不可重復
}
/*
* 2.用戶登陸
*
* @param userid 用戶id
* @param userpassword 用戶密碼
* @return 用戶所有資訊并以JSON格式回傳
*
* 測驗舉例 引數一:"191867345212322,123456"
*/
function Login(string memory _userid,string memory _userpassword) public view returns (int8,string) {
string memory _passwordhash=TypeConvertUtil.bytes32ToString(sha256(abi.encode(_userid,_userpassword)));
return loginInToJson(t_user_struct,_userid,_passwordhash);
}
/*
* 3.查詢用戶資訊并以字串陣列方式輸出
*
* @param _userid 用戶id
*
* @return 執行狀態碼
* @return 該用戶資訊的字串陣列
*
* 測驗舉例 引數一:"191867345212322"
*/
function getUserRecordArray(string userid) public view returns(int8, string[]){
return selectOneRecordToArray(t_user_struct, userid, ["user_id",userid]);
}
}
2使用實體:
- 用戶注冊:
通過呼叫activateUser傳入用戶的ID、姓名、密碼、身份類別完成注冊,

-
查詢用戶資訊:
? 因為區塊鏈上的資訊是公開透明的,因此不應當把密碼等隱私資料直接上鏈,而應當對隱私資料進行**“脫敏上鏈”**,在這里我們對用戶的密碼進行了hash處理,由于哈希函式具有單向性,因此即使該哈希值被他人讀取了也很難破解用戶的密碼,

-
用戶注冊日志記錄:

3.1.2 前端代碼
? 前端界面主要分為用戶注冊頁面和用戶登錄頁面.這里主要講一下注冊頁面.通過一個form表單讓用戶輸入關鍵的資訊,注冊時候,能自動獲取的資訊就不讓用戶手動再輸入一遍,增加用戶的使用體驗,自動獲取用戶的微信昵稱作為用戶的賬戶名,用戶需要手工輸入6-20位的密碼,再通過復選框cu-form-group選擇三類身份中的一種,最后點擊提交即可完成注冊,
<view class="zan-dialog {{ showDialog ? 'zan-dialog--show' : '' }}">
<view class="zan-dialog__mask" bindtap="toggleDialog" />
<view class="zan-dialog__container">
<view class="padding flex flex-direction">
//用戶注冊,輸入需要的資訊
<view class="text-center text-xl text-bold text-cyan margin-top">{{registeredStatus?'登陸后獲得發布和下載權限':'注冊后獲得發布和下載權限'}}</view>
<view class="cu-form-group margin-top">
<view class="title">用戶賬號</view>
<input disabled="true" value="{{userNickName}}"></input>
</view>
<view class="cu-form-group">
<view class="title">輸入密碼</view>
<input placeholder="請輸入6~20位的密碼" password="true" maxlength="20" value="{{password}}" bindinput='getPassword'></input>
</view>
<view class="cu-form-group" wx:if="{{!(isLogin || registeredStatus)}}">
<view class="title">確認密碼</view>
<input placeholder="請重新輸入密碼" password="true" maxlength="20" value="{{passwordToCheck}}" bindinput='getPasswordToCheck'></input>
</view>
//用戶身份選擇可以用下拉框.分為三類個人、企業、政府
<view class="cu-form-group" wx:if="{{!(isLogin || registeredStatus)}}">
<view class="title">選擇用戶身份</view>
<picker bindchange="changeUserType" value="{{userType}}" range="{{userTypeList}}">
<view class="picker">
{{userTypeList[userType]}}
</view>
</picker>
</view>
<button class="cu-btn bg-cyan margin-top lg" bindtap="checkPasswordAndPost" wx:if="{{!registeredStatus}}">提交</button>
<button class="cu-btn bg-cyan margin-top lg" bindtap="login" wx:if="{{!isLogin&®isteredStatus}}">登陸</button>
</view>
</view>
</view>
前端界面效果圖如下:
-
注冊界面:
-
登陸界面:
3.1.3 后端代碼
? 后端根據用戶微信登錄后產生唯一的openId自動作為用戶的Id,作為用戶注冊的唯一主鍵,通過該主鍵獲取用戶的微信號、微信昵稱、密碼哈希、微信頭像、注冊狀態、信用積分、用戶身份類別、下載檔案記錄等資訊,
class User(models.Model):
#自動生成openId作為用戶的唯一ID
openId = models.CharField(db_index=True, max_length=100, default='0', null=True) # 唯一表示微信用戶的id
userWxName = models.CharField(verbose_name='用戶微信名', max_length=40, null=True) # 用戶微信名
userLoginName = models.CharField(verbose_name='用戶登錄名', max_length=10, null=True)
userPassword = models.CharField(verbose_name='密碼通過sha256計算后的hash值', max_length=64, null=True)
avatarUrl = models.URLField(max_length=255, null=True) # 用戶頭像
activateTime = models.DateTimeField(verbose_name='注冊時間', null=True)
registeredStatus = models.BooleanField(verbose_name='注冊狀態', default=False)
userCreditPoint = models.IntegerField(verbose_name='信用積分', default=100)#給定默認信用積分為100分
userTypeChoices = [(0, '個人'), (1, '企業'), (2, '政府機構')] #通過用戶在前端界面選擇的序號確定身份類別
userType = models.PositiveSmallIntegerField(verbose_name='用戶型別', choices=userTypeChoices, null=True, blank=True)
downloadRecord = models.ManyToManyField("OfficeFile", blank=True, through='DownloadRecord',
through_fields=('user', 'officeFile'), related_name='userDownloadRecord')
#獲取用戶資訊
def getUserInfo(self,request):
obj = json.loads(request.body)
user = get_user(obj)
if not user:
return HttpResponse('false') # token過期
else:
if obj['funType'] == 0:
user.userWxName = obj['name']
user.avatarUrl = obj['avatarUrl']
user.save()
userTypeList = []
choices = User.userTypeChoices
for c in choices:
userTypeList.append(c[1])
data = {'userType': user.userType, 'userTypeList': userTypeList}
if obj['funType'] == 1:
data['avatarUrl'] = user.avatarUrl
data['nickName'] = user.userWxName
return JsonResponse(data, safe=False)
3.2 建言獻策模塊
? 每位用戶可在建言獻策界面的文本框內輸入建言標題和建言內容,確保內容無誤后點擊“提交建議”,建言資料將上傳到本地資料庫和區塊鏈上,后端呼叫 API 對用戶所發布的內容進行違規詞檢測替換,若內容有不文明用詞將扣除用戶 1 點信用值,且將建言標題內容和信用扣除記錄上傳到區塊鏈上(用戶違規事件記錄上鏈),每位用戶可在小程式首頁瀏覽所有用戶提交的建議,且每條 建議會顯示建議提交者 ID、提交時間以及是否違規,同時也可在個人中心的信用值記 錄查看自己提交的建言的詳細記錄
3.2.1 合約代碼
**1.功能說明:**本合約實作功能主要為:1.用戶留言 2.查看用戶留言記錄
suggest(string memory _proposeid,string memory _userid,string memory _title,string memory _content):用戶留言:傳入留言內容Id、用戶的ID號、留言標題、留言內容,getSuggestRecordJson(string _proposeid):回傳留言內容:傳入留言內容ID號,以json形式回傳留言內容
pragma solidity ^0.4.25;
pragma experimental ABIEncoderV2;
import "../lib/SafeMath.sol";
import "../utils/TimeUtil.sol";
import "./TableDefTools.sol";
/*
*
* UserSuggest實作用戶留言功能,
* 首先查看用戶是否注冊,登陸,如果沒有則不能留言
* 操作的表為t_suggest.
*
*/
contract Suggest is TableDefTools{
/******* 引入庫 *******/
using TimeUtil for *;
using SafeMath for *;
event SUGGEST(string Proposeid,string Userid,string _title,string SuggestContent,string time);//留言事件.留言人ID,留言標題,內容,時間
/*
* 建構式,初始化使用到的表結構
*
* @param 無
*
* @return 無
*/
constructor() public{
//初始化需要用到的表,建言獻策表
initTableStruct(t_propose_struct, TABLE_PROPOSE_NAME, TABLE_PROPOSE_PRIMARYKEY, TABLE_PROPOSE_FIELDS);
}
/*
* 1.用戶留言
*
* @param _proposeid 留言ID號
* @param _userid 留言人ID
* @param _title 留言內容標題
* @param _content 留言內容
*
* @return 執行狀態碼
*
* 測驗舉例 引數一:"17846"
* 引數二:"191867345212322","關于提供退役軍人金融優惠措施的建議","尊敬的領導,本人通過網路知道了了去年事務部和十大銀行簽了優撫協議,其它省份落實的很好,希望江西省也能對接下江西銀行,江西農商銀行等"
*注冊成功回傳SUCCESS,以及留言的句子,否則回傳錯誤碼,錯誤碼對應的問題請參考TableDefTools寫的
*/
function suggest(string memory _proposeid,string memory _userid,string memory _title,string memory _content) public returns(int8){
//獲取時間
string memory nowDate = TimeUtil.getNowDate();
string memory suggestfields=StringUtil.strConcat4("建議標題為:",_title,"建議內容為:",_content);
string memory storeFields = StringUtil.strConcat5(_userid,',',suggestfields,',',nowDate);
emit SUGGEST(StringUtil.strConcat2("留言的ID號為:",_proposeid),StringUtil.strConcat2("留言人的ID為:",_userid),
StringUtil.strConcat2("留言標題為:",_title),StringUtil.strConcat2("留言內容為:",_content),StringUtil.strConcat2("留言時間為:",nowDate));
return (insertOneRecord(t_propose_struct,_proposeid,storeFields,false));
}
/*
* 2.根據留言ID號查詢留言人ID號和留言內容并以Json字串方式輸出
*
* @param _proposeid 留言id
*
* @return 執行狀態碼
* @return 該用戶所有留言資訊的Json字串
*
* 測驗舉例 引數一:"17846"
*/
function getSuggestRecordJson(string _proposeid) public view returns(int8, string){
return selectOneRecordToJson(t_propose_struct, _proposeid);
}
2使用實體:
-
用戶留言:

-
用戶留言成功的回執資訊

-
通過留言內容的Id號查看留言資訊


-
用戶留言事件在區塊鏈端的日志記錄

-
用戶違規發言的扣分記錄

3.2.2 前端代碼
-
用戶提交留言:用戶在表單中輸入留言標題和留言內容,通過
placeholder給用戶提供輸入內容事例,防止用戶不知道該如何留言,表單設定容納最大大小為500字,防止內容過多,<!--pages/suggest/addSuggest/addSuggest.wxml--> <view class="cu-form-group"> <!--用戶輸入留言的標題和內容,后臺通過js請求生成留言Id號并獲取用戶Id--> <view class="title">建言標題</view> <input placeholder="如:審批流程問題建議" maxlength="50" value="{{suggestTitle}}" bindinput="getSuggestTitle"></input> </view> <view class="cu-form-group align-start" style="height: 500rpx;"> <view class="title">建言內容</view> <!--給出用戶示例輸入--> <textarea maxlength="-1" value="{{suggestContent}}" placeholder="請輸入政府辦公流程體制記憶體在的具體問題及有可能解決該問題的建議" bindinput="getSuggestContent" style="height: 450rpx;"></textarea> </view> <view class="btn-area padding-xl"> <!--兩個按鈕.用戶確認無誤點擊提交,否則點擊重新填寫--> <button class="cu-btn block bg-cyan lg" style="width: 500rpx; height: 80rpx;" bindtap="postSuggest"><text class="cuIcon-upload" style="margin-right: 7rpx;"></text>提交建議</button> <button class="cu-btn block bg-grey margin-tb-sm lg" style="width: 500rpx; height: 80rpx;" bindtap="resetSuggest"><text class="cuIcon-refresh" style="margin-right: 7rpx;"></text>重新填寫</button> </view> -
用戶留言內容展示:后端通過
http請求將用戶留言的標題、內容、留言時間、上傳用戶的Id、留言內容是否合規等資訊展示到前端來,這里對用戶留言合規用綠色表示,如果不合格用紅色表示,起到醒目的作用,
<!--pages/suggest/suggestInfo/suggestInfo.wxml-->
<view class="cu-bar bg-white" style="margin: 0rpx 0 1rpx 0;">
<view class="action">
<text class="cuIcon-title text-green"></text>
<text>建言標題</text>
</view>
</view>
<view class="padding bg-white text-bold">{{title}}</view>
<view class="cu-bar bg-white" style="margin: 1rpx 0 1rpx 0;">
<view class="action">
<text class="cuIcon-title text-green"></text>
<text>建言內容</text>
</view>
</view>
<view class="padding bg-white" style="margin: 0rpx 0 2rpx 0;text-align: justify;">{{content}}</view>
<view class="cu-form-group">
<view class="title">留言時間</view>
<view>{{suggestTime}}</view>
</view>
<view class="cu-form-group">
<view class="title">上傳用戶ID</view>
<view class="text-right">{{userId}}</view>
</view>
<view class="cu-form-group">
<view class="title">內容是否合規</view>
<view class="text-{{isCompliance=='內容合規'?'green':'red'}}">{{isCompliance}}</view>
</view>
-
用戶留言前端界面圖:
-
用戶查看自己的留言記錄圖:
-
用戶瀏覽他人留言:
-
用戶查看留言內容詳情:
3.2.3 后端代碼
后端對用戶留言的內容進行判斷,如果標題為慷訓者內容為空,則回傳error給用戶,提醒用戶輸入為空,對用戶留言的內容與從github搜集的違規詞語料庫進行匹配,若用戶的留言內容違規,那么將呼叫api對用戶的信用積分進行扣分處理,
def addSuggest(request):
obj = json.loads(request.body)
user = get_user(obj)
if not user:
return HttpResponse('false') # token過期
else:
suggest = Suggest(user_id=user.id, suggestTitle=obj['suggestTitle'], suggestContent=obj['suggestContent'])
suggest.save()
addSuggestToChain(suggest.id, user.userLoginName, suggest.suggestTitle, suggest.suggestContent)
content = getSuggestFromChain(suggest.id)[1]
suggestFromChain = content[1]
titleStartIndex = re.search('建議標題為:', suggestFromChain).span()[1]
contentStartIndex = re.search('建議內容為:', suggestFromChain).span()[1]
data1 = checkContent(suggestFromChain[titleStartIndex:contentStartIndex - 6])
data2 = checkContent(suggestFromChain[contentStartIndex:])
suggest.changeSuggestTitle = data1['text']
suggest.changeSuggestContent = data2['text']
if data1['num'] > 0 or data2['num'] > 0:
suggest.isCompliance = False
user.userCreditPoint = user.userCreditPoint - 1
user.save()
else:
suggest.isCompliance = True
suggest.save()
if suggest.isCompliance is False:
suggesterId = getSuggesterId(suggest.id)
updateUserCredit(suggesterId, 1, 0)
return HttpResponse("success")
#判斷用戶的留言內容是否違規,從github上搜集違規詞存入keywords.txt檔案中,如果用戶留言內容在違規詞內則判定為違規
def checkContent(content):
gfw = DFAFilter()
gfw.parse("keywords.txt")
number = len(content.split("*"))-1
text = gfw.filter(content, "*")
data = {"num": len(text.split("*"))-1-number, "text": text}
return data
#獲取用戶留言細息,展示到前端頁面
def returnSuggestList(request):
obj = json.loads(request.body)
pageSize = 10
currentPage = obj['currentPage']
startRow = (currentPage - 1) * pageSize
endRow = currentPage * pageSize
suggests = Suggest.objects.all().order_by('-id')[startRow:endRow]
suggestList = []
#遍歷所有的留言記錄,取出留言的Id號、留言標題、留言內容、是否違規、留言時間
for s in suggests:
obj = {"id": s.id, "userId": s.user.userLoginName, 'title': s.changeSuggestTitle,
"content": s.changeSuggestContent,
"isCompliance": s.isCompliance, "suggestTime": s.suggestTime.strftime("%Y.%m.%d")}
suggestList.append(obj)
return JsonResponse({"suggestList": suggestList})
3.3 巡檢模塊
? 該模塊模擬了在實際生活中,政府部門經常會有一些任務,要求在什么時候去哪些 地方巡查,也就涉及到用戶打卡,我們將用戶打卡記錄上鏈,避免了代打卡,甚至篡改資料庫等問題,同時領導可以通過下屬的用戶 ID查看他的打卡記錄資訊,
3.3.1 合約代碼
**1.功能說明:**本合約實作功能主要為:1.用戶打卡2.查看用戶打卡資訊記錄
-
ClockIn(string memory _userid,string memory _location,string memory _time)實作用戶打卡功能:傳入用戶Id號,打卡地點,打卡時間 -
getUserClockInfo(string _userid)查看用戶打卡記錄;傳入用戶Idpragma solidity ^0.4.25; import "../utils/TimeUtil.sol"; import "../utils/StringUtil.sol"; import "./TableDefTools.sol"; pragma experimental ABIEncoderV2; contract Track is TableDefTools{ /******* 引入庫 *******/ using TimeUtil for *; using StringUtil for *; /* * Track實作用戶打卡巡檢合約 * 主要包括:1.用戶打卡2.查看用戶打卡記錄回傳JSON陣列 * */ /* * 建構式,初始化使用到的表結構 * * @param 無 * * @return 無 */ constructor() public{ //初始化需要用到的表,資料資源表 initTableStruct(t_track_struct, TABLE_TRACK_NAME, TABLE_TRACK_PRIMARYKEY, TABLE_TRACK_FIELDS); } //定義事件日志資訊 event TRACK_EVENT(string user_id,string user_location,string time);//用戶打卡記錄日志,誰在哪里什么時間打的卡 /* * 1.用戶打卡 * * @param _userid 用戶id * @param _location 地點名 * @param _time 打卡時間 * @return 執行狀態碼 * * 測驗舉例 引數一:"191867345212322" * 引數二:"江西省人民政府","2020年1月19日15點58分29秒" *注冊成功回傳SUCCESS否則回傳錯誤碼,錯誤碼對應的問題請參考DB */ function ClockIn(string memory _userid,string memory _location,string memory _time) public returns(int8){ string memory storeFields=StringUtil.strConcat3(_location,',',_time); emit TRACK_EVENT(StringUtil.strConcat2("用戶ID:",_userid),StringUtil.strConcat2("打卡地點:",_location),StringUtil.strConcat2("到達時間:",_time)); return insertOneRecord(t_track_struct,_userid,storeFields,true); } /* * 2.查詢用戶打卡記錄并以JSON方式輸出 * * @param _userid 用戶id * * @return 執行狀態碼 * @return 該用戶去過的地方的JSON陣列 * * 測驗舉例 引數一:"191867345212322" */ function getUserClockInfo(string _userid) public view returns(int8, string){ return selectOneRecordToJson(t_track_struct,_userid); } }2 使用實體:
- 用戶打卡:小程式自動獲取用戶的Id唯一主鍵、呼叫
wx.getLocation(Object object)獲取用戶
的打卡地點.

用戶成功打卡完成后回傳資訊如下:

-
通過輸入用戶的Id以json的形式回傳用戶的打卡資訊:
查詢輸入:

查詢成功回傳如下圖所示,查詢失敗回傳錯誤碼

- 用戶打卡:小程式自動獲取用戶的Id唯一主鍵、呼叫
用戶打卡資訊日志記錄:

3.3.2 前端代碼
? 用戶打卡界面以兩個按鈕為主,點擊授權獲取地址按鈕可以呼叫微信的獲取用戶地址的介面,得到當前所在位置的經緯度,查看無誤后,點擊打卡即可,
<!--用戶打卡的前端頁面-->
<view class="zan-dialog zan-dialog--show">
<view class="zan-dialog__mask" />
<view class="zan-dialog__container">
<view class="padding flex flex-direction">
<view class="text-center text-xl text-bold text-cyan margin-bottom">當前地址</view>
<view class="text-center text-bold">{{localText}}</view>
<button class="cu-btn bg-cyan margin-top lg" style="width: 500rpx; height: 80rpx;"
bindtap="postAddressOnChain"><text class="cuIcon-check" style="margin-right: 7rpx;"></text>打卡</button>
<button class="cu-btn bg-cyan margin-top lg" style="width: 500rpx; height: 80rpx;" open-type="openSetting" bindtap="locationAuthorization"><text class="cuIcon-unlock" style="margin-right: 7rpx;"></text>授權獲取地址</button>
</view>
</view>
</view>
-
用戶打卡的前端界面圖如下:
-
查詢用戶打卡資訊:
查詢用戶打卡頁面在頁面上方設定了一個搜索框,用戶可以輸入查詢的用戶Id賬號,點擊搜索后呼叫合約介面,回傳用戶的打卡記錄,即用戶的打卡地點和打卡時間,
<!--查詢用戶打卡資訊的前端頁面--> <view id="search"> <input id="input" placeholder="如:1611628666" placeholder-class="placeholderClass" value="{{searchValue}}" bindinput="getSearchValue"></input> <view id="comfirm" bindtap="search">搜索</view> </view> <view class="cu-bar padding-top-sm" style="min-height: 80rpx; border-bottom: 2rpx solid #f1f1f1; margin-top: 100rpx;" wx:if="{{hadSearch}}"> <view class="action"> <text class="margin-left cuIcon-title text-cyan"></text> <text class="text-black text-bold">用戶打卡記錄</text> </view> </view> <view class="cu-card article" style="height: auto;" wx:for="{{trackList}}" wx:key='id' wx:if="{{hadSearch}}"> <view class="cu-item shadow"> <view class="title" style="line-height: 80rpx;"> <view class="text-cut">{{item.fields.user_location}}</view> </view> <view class="content" style="height: auto;"> <!-- <view class="desc"> --> <view class="cu-tag bg-white light sm round">{{item.fields.arrival_time}}</view> <!-- </view> --> </view> </view> </view> <view id="blankArea" style="width: 100%;position: absolute;top: 470rpx;display: flex;flex-direction: column;align-items: center;justify-content: center;" wx:if="{{trackList.length == 0 && hadSearch}}"> <image id="blankImage" style="width: 114rpx;height: 100rpx;margin-bottom: 30rpx;" src="/images/blank.png"></image> <view id="blankText" style="font-size: 26rpx;color: #999999;text-align: center;white-space:pre-line;">該用戶無打卡記錄 </view> </view>
3.3.3 后端代碼
? 后端呼叫wx.getLocation獲取用戶當前定位的經緯度.將該資訊以及用戶的Id通過合約api介面呼叫,存入區塊鏈上,若用戶未授權獲取定位點擊了打卡將會通過wx.showModel彈窗的形式給用戶提醒未能獲得您的定位,請點擊“授權獲得地址”按鈕以獲得定位
//呼叫wx.getLocation獲取用戶當前定位的經緯度
showLocal: function () {
var that = this;
wx.getLocation({
type: 'gcj02',
success: function (res) {
let latitude = res.latitude//經度
let longitude = res.longitude//緯度
that.setData({
latitude,
longitude
})
that.getMapCity(latitude, longitude)
}
})
},
postAddressOnChain: function(){
var that = this
if(!that.data.canClockIn){
wx.showModal({
title: '溫馨提示',
content: '未能獲得您的定位,請點擊“授權獲得地址”按鈕以獲得定位',
showCancel: false,
success (res) {
if (res.confirm) {
console.log('用戶點擊確定')
} else if (res.cancel) {
console.log('用戶點擊取消')
}
}
})
return
} else {
const app = getApp()
//獲取時間
var date = new Date()
var year = date.getFullYear() + '年'
var month = date.getMonth() + 1 + '月'
var day = date.getDate() + '日'
var hour = date.getHours() + '點'
var minute = date.getMinutes() + '分'
var second = date.getSeconds() + '秒'
var dateText = year + month + day + hour + minute + second
//將資訊上鏈
var objToChain = {
"groupId" :5,
"signUserId": "fee97843cf0c45d683bade8fdebe724f",
"contractAbi":[{"constant":true,"inputs":[{"name":"_userid","type":"string"}],"name":"getUserCityArray","outputs":[{"name":"","type":"int8"},{"name":"","type":"string[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_userid","type":"string"}],"name":"getUserCityJson","outputs":[{"name":"","type":"int8"},{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_fields","type":"string[]"},{"name":"index","type":"uint256"},{"name":"values","type":"string"}],"name":"getChangeFieldsString","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_userid","type":"string"},{"name":"_location","type":"string"},{"name":"_time","type":"string"}],"name":"ClockIn","outputs":[{"name":"","type":"int8"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"user_id","type":"string"},{"indexed":false,"name":"user_location","type":"string"},{"indexed":false,"name":"time","type":"string"}],"name":"TRACK_EVENT","type":"event"}],
"contractAddress":"0x8546174c5fe38243e1dcfb65e1347919fe0f45ba",
"funcName":"ClockIn",
"funcParam":[app.globalData.idNumber,that.data.localText,dateText],
"useCns":false
}
//通過http請求與鏈上資料進行互動
http.httpToChain(objToChain, function (res) {
if(res.message == 'success' || res.message == 'Success'){
wx.showToast({
title: '打卡成功',
icon: 'success',
duration: 2000
})
}
})
}
},
3.4 政務合作模塊
? 政府要發布某一條訊息往往不是某一個人決定的,而是多級領導審核后同意才會通過,在檔案傳輸程序中,如何保障資料不被篡改,區塊鏈就可以起到很大的作用,但是區塊鏈存盤資料對資源消耗特別大,因此我們決定對資料**“輕裝”上鏈**, 政府公文檔案的 pdf 放鏈下本地資料庫,檔案的哈希值上鏈,需要驗證的時候將 pdf 再做一次哈希運算與鏈上對比,一致則可以保證檔案的未被篡改,
3.4.1 合約代碼
**1.功能說明:**本合約實作功能主要為:1.科員提交材料2.查詢申請材料當前狀態 并以Json字串方式輸出3.領導簽字圖片資訊,審核結果資訊上鏈,4.將審核結果決定是否公示
applyForDocument(string memory _applicationid,string memory _userid,string memory _informationhash):傳入檔案的Id號、申請人Id、檔案的哈希值getApplyJson(string memory _applicationid):傳入檔案的Id號,以Json形式回傳檔案資訊checkApplyResearch(string memory _applicationid,string memory checkhash):檔案Id號、領導簽字圖片哈希值GiveResultToUser(string _checkerid,string memory _applicationid,string memory checkresult):審核人Id號、材料Id、審核結果
pragma solidity ^0.4.25;
pragma experimental ABIEncoderV2;
import "../utils/TimeUtil.sol";
import "./TableDefTools.sol";
/*
* GovCooperation模擬政府辦公協作的流程
* 首先科員提交材料申請,等待上級審批是否通過,通過后公示給大眾
*
*/
contract GovCooperation is TableDefTools{
/******* 引入庫 *******/
using TimeUtil for *;
/*
* 建構式,初始化使用到的表結構
*
* @param 無
*
* @return 無
*/
constructor() public{
//初始化需要用到的表,檔案材料申請審核
//公示政府檔案表
initTableStruct(t_application_struct, TABLE_APPLICATION_NAME, TABLE_APPLICATION_PRIMARYKEY, TABLE_APPLICATION_FIELDS);
}
//
event APPLY_DOCUMENT_EVENT(string userid,string application_id,string date);
event Track_LeaderSignature(string applicationid,string signHash,string date);//記錄領導簽字
event Tack_Final_Result(string applicationid,string checkerid,string result,string date);//記錄最后校驗部結果
/*
* 1.科員提交材料,具體材料放鏈下,鏈上存放申請材料哈希值,加密演算法采用的sha256目前最安全的一種演算法,暫時不可能逆推
*
* @param _applicationid 材料id唯一主鍵
* @param _fields 用戶資訊表各欄位值拼接成的字串包括如下:
* 材料提交用戶ID
* 申請材料哈希值
* 申請時間 【呼叫輪子自動生成】
* 【后三個欄位, 代表 領導簽字哈希值 , 審核結束時間,審核結果,最初默認值為“審批中,審批中,審批中”】后面修改
*
* @return 執行狀態碼
*
* 測驗舉例 引數一:"1976945"
* 引數二:"1611408890,f44e39c1fc14dc05143eeba2065a921bbbc1bba5"
*注冊成功回傳SUCCESS否則回傳錯誤碼,錯誤碼對應的問題請參考DB
*/
function applyForDocument(string memory _applicationid,string memory _userid,string memory _informationhash) public returns(int8){
// 獲得當前的日期
string memory nowDate = TimeUtil.getNowDate();
string memory firstTwoFields=StringUtil.strConcat5(_userid,',',_informationhash,',',nowDate);
string memory lastFourParams = ",審核中,審核中,審核中";
string memory storeFields = StringUtil.strConcat2(firstTwoFields,lastFourParams);
emit APPLY_DOCUMENT_EVENT(StringUtil.strConcat2("申請人的ID號為:",_userid),StringUtil.strConcat2("材料的ID為:",_applicationid),StringUtil.strConcat2("申請時間為:",nowDate));
return insertOneRecord(t_application_struct,_applicationid,storeFields,false);//最后的false代表主鍵下記錄不可重復
}
/*
* 2.查詢申請材料當前狀態 并以Json字串方式輸出
*
* @param _applicationid 材料id唯一主鍵
*
* @return 執行狀態碼
* @return 該用戶資訊的字串陣列
*
* 測驗舉例 引數一:"1976945"
*/
function getApplyJson(string memory _applicationid) public view returns(int8, string){
return selectOneRecordToJson(t_application_struct,_applicationid);
}
/*
* 3.領導簽字圖片資訊,審核結果資訊上鏈,具體材料放鏈下,鏈上存放申請材料哈希值,加密演算法采用的sha256目前最安全的一種演算法,暫時不可能逆推
* 具體操作為修改申請表的check_hash欄位
* @param _applicationid 材料id唯一主鍵
* @param checkhash 領導簽字圖片哈希值
*
* @return 執行狀態碼
*
* 測驗舉例 引數一:"1976945"
* 引數二:"f88r46f6ki14dc05143eeba2065a921bbbc1bbqi5"
*注冊成功回傳SUCCESS否則回傳錯誤碼,錯誤碼對應的問題請參考DB
*/
function checkApplyResearch(string memory _applicationid,string memory checkhash) public returns(int8){
// 獲得當前的日期
string memory nowDate = TimeUtil.getNowDate();
//查詢用戶申請資訊回傳狀態
int8 queryRetCode;
//更新用戶申請保送表后回傳狀態
int8 updateRetCode;
// 資料表回傳資訊
string[] memory retArray;
// 查看該用戶申請審核資訊
(queryRetCode, retArray) = selectOneRecordToArray(t_application_struct, _applicationid, ["application_id", _applicationid]);
// 若存在該用戶記錄
if(queryRetCode == SUCCESS_RETURN){
string memory changedFieldsStr = getChangeFieldsString(retArray,3,checkhash);
updateRetCode = (updateOneRecord(t_application_struct,_applicationid,changedFieldsStr));
if(updateRetCode == SUCCESS_RETURN){
//記錄日志
emit Track_LeaderSignature(StringUtil.strConcat2("材料Id為:",_applicationid),
StringUtil.strConcat2("領導簽字圖片的哈希為:",checkhash),StringUtil.strConcat2("申請時間為:",nowDate));
return SUCCESS_RETURN;
}
else{
return FAIL_RETURN;
}
}else{
return FAIL_RETURN;
}
}
/*
* 4.校驗部驗證完以后,把是否公開結果上鏈,
* @param _checkerid 校驗人員id
* @param _applicationid 材料id唯一主鍵
* @param checkresult 該檔案是否通過審批,公示給大眾,通過/不通過:
*
* @return 執行狀態碼
*
* 測驗舉例 引數一:"186789,1976945,"通過""
*注冊成功回傳SUCCESS否則回傳錯誤碼,錯誤碼對應的問題請參考DB
*/
function GiveResultToUser(string _checkerid,string memory _applicationid,string memory checkresult) public returns(int8){
//查詢檔案資訊回傳狀態
int8 queryRetCode;
//更新檔案后回傳狀態
int8 updateRetCode;
// 資料表回傳資訊
string[] memory retArray;
// 獲得當前的日期
string memory nowDate = TimeUtil.getNowDate();
// 查看該科員申請審核資訊
(queryRetCode, retArray) = selectOneRecordToArray(t_application_struct, _applicationid, ["application_id", _applicationid]);
// 若存在該用戶記錄
if(queryRetCode == SUCCESS_RETURN){
//修改科員申請表中的審核時間
string memory changedFieldsStr = getChangeFieldsString(retArray, 5, nowDate);
//修改
updateRetCode = (updateOneRecord(t_application_struct, _applicationid,changedFieldsStr));
if(updateRetCode == SUCCESS_RETURN){
string memory changedFieldsStr2 = getChangeFieldsString(retArray, 4, checkresult);
emit Tack_Final_Result(StringUtil.strConcat2("審核人Id為:",_checkerid),StringUtil.strConcat2("審核材料id為:",_applicationid),
StringUtil.strConcat2("審核結果為:",checkresult),StringUtil.strConcat2("審核完成時間為:",nowDate));
return (updateOneRecord(t_application_struct,_applicationid,changedFieldsStr2));
}
}
else{
// 若不存在該科員提交記錄
return FAIL_RETURN;
}
}
}
2 使用實體:
-
科員上傳檔案
-
輸入檔案id號查詢檔案狀態

-
領導簽字哈希上鏈:
? 
成功回執資訊:

-
審核員審批.校驗檔案沒被篡改、且領導意見為通過、領導簽名未被篡改后,提交最終審批結果如下:

交易回執:

-
科員提交檔案日志資訊:

-
領導審批日志資訊:
-

-
審核結果日志資訊:

3.4.2 前端代碼
? 前端以一個表單的形式讓用戶手工輸入檔案的名稱和內容,另外選擇檔案(支持doc和pdf等格式),選擇完后點擊上傳即可,領導審核的時候,將下載下來該檔案,并計算檔案的哈希值與區塊鏈上存盤的檔案哈希值作比對,一致則說明檔案沒發生篡改,才可以進行后面的步驟,否則將會報錯,
<!--pages/resourceView/resourceInfo/resourceInfo.wxml-->
<!--填寫檔案資訊-->
<view class="cu-bar bg-white padding-top-sm" style="min-height: 80rpx; border-bottom: 2rpx solid #f1f1f1;">
<view class="action">
<text class="cuIcon-title text-cyan"></text>
<text class="text-black text-bold">檔案名稱</text>
</view>
</view>
<view class="cu-bar bg-white" style="min-height: 80rpx; border-bottom: 2rpx solid #f1f1f1;">
<view class="action" style=" margin:0 67rpx 0 67rpx;">
<text>{{title}}</text>
</view>
</view>
<view class="cu-bar bg-white" style="min-height: 80rpx; border-bottom: 2rpx solid #f1f1f1;">
<view class="action">
<text class="cuIcon-title text-cyan"></text>
<text class="text-black text-bold">檔案介紹</text>
</view>
</view>
<view class="cu-bar bg-white" style="min-height: 20rpx; border-bottom: 2rpx solid #f1f1f1;">
<view class="action padding text-content" style=" margin:0 37rpx 0 37rpx;">
<text>{{introduction}}</text>
</view>
</view>
<view class="cu-bar bg-white" style="min-height: 80rpx; border-bottom: 2rpx solid #f1f1f1;">
<view class="action">
<text class="cuIcon-title text-cyan"></text>
<text class="text-black text-bold">上傳時間:</text><text>{{uploadTime}}</text>
</view>
</view>
<view class="cu-bar bg-white" style="min-height: 80rpx; border-bottom: 2rpx solid #f1f1f1;">
<view class="action">
<text class="cuIcon-title text-cyan"></text>
<text class="text-black text-bold">資料提供者ID:</text><text>{{userId}}</text>
</view>
</view>
<view class="cu-bar bg-white" style="min-height: 80rpx; border-bottom: 2rpx solid #f1f1f1;">
<view class="action">
<text class="cuIcon-title text-cyan"></text>
<text class="text-black text-bold">資料總下載次數:</text><text>{{downloadNum}}</text>
</view>
</view>
<!--通過把檔案的哈希值與該檔案的id對應的區塊鏈端的哈希值做對比保障檔案未被篡改-->
<view class="text-center margin-top text-gray">校驗可保證你下載的檔案并未被他人修改</view>
<view class="btn-area padding-xl">
<button class="cu-btn block {{buttonBg}} lg" style="width: 550rpx; height: 80rpx;" bindtap="downloadDataAndCheck"
disabled="{{loading}}"><text class="{{buttonIcon}}" style="margin-right: 7rpx;"></text>{{buttonText}}</button>
</view>
-
科員提交檔案界面圖:
-
科員查看檔案審核進度:
-
科長簽字審核:
-
處長審核:
-
檔案公示:
3.4.3 后端代碼
? 后端代碼分為這幾個功能1.查看檔案的狀態;2.將領導審核的意見存盤上鏈3.將領導審核的簽名哈希上鏈;通過這些功能來保障資訊是沒有被篡改過的,做到協同辦公,最后將檔案審核結果公示給用戶,
# 查看檔案當前狀態
def returnApplyStatus(request):
obj = json.loads(request.body)
user = get_user(obj)
if not user:
return HttpResponse('false') # token過期
else:
#通過檔案的id查詢檔案的當前狀態
a = OfficeFile.objects.get(id=obj['fileId'])
data = {'uploadTime': a.uploadTime.strftime("%Y.%m.%d"),
"informationLink": a.informationLink, "informationHash": a.informationHash,
"boss1Opinion": a.boss1Opinion, "boss1SignLink": a.boss1SignLink, "boss1SignHash": a.boss1SignHash,
"boss2Opinion": a.boss2Opinion, "boss2SignLink": a.boss2SignLink, "boss2SignHash": a.boss2SignHash,
"checkHash": a.checkHash, "researchResult": a.researchResult, 'reviewResult': a.reviewResult
}
#以json形式回傳結果
return JsonResponse(data)
#領導審核
def postApplicationListToAdminToSign(request): # 領導簽字串列推送
obj = json.loads(request.body)
user = get_user(obj)
if not user:
return HttpResponse('false') # token過期
else:
whichBoss = obj['identity']
departmentId = obj['departmentId']
pageSize = 10
currentPage = obj['currentPage']
startRow = (currentPage - 1) * pageSize
endRow = currentPage * pageSize
if whichBoss == 1:
officeFiles = OfficeFile.objects.filter(workingStatus=False, boss1Opinion=None,
officeMember__department__id=departmentId).order_by('-id')[
startRow:endRow]
else:
officeFiles = OfficeFile.objects.filter(workingStatus=False, boss2Opinion=None,
officeMember__department__id=departmentId).exclude(
boss1Opinion=None).order_by('-id')[startRow:endRow]
officeFileList = []
for o in officeFiles:
data = {"fileId": o.id, 'uploadTime': o.uploadTime.strftime("%Y.%m.%d"),
"informationLink": o.informationLink, "informationHash": o.informationHash, 'title': o.dataTitle,
"introduction": o.dataIntroduction, 'userId': o.officeMember.user.userLoginName}
officeFileList.append(data)
return JsonResponse({"officeFileList": officeFileList})
#將領導簽字圖片的哈希值上鏈
def bossSign(request):
obj = json.loads(request.body)
user = get_user(obj)
if not user:
return HttpResponse('false') # token過期
else:
whichBoss = obj['whichBoss']
applicationForm = OfficeFile.objects.get(id=obj['id'])
if whichBoss == 1:
applicationForm.boss1Opinion = obj['boss1Opinion']
applicationForm.boss1SignLink = obj['boss1SignLink']
applicationForm.boss1SignHash = obj['boss1SignHash']
if obj['boss1Opinion'] is False:
applicationForm.researchResult = False
applicationForm.workingStatus = False
else:
applicationForm.boss2Opinion = obj['boss2Opinion']
applicationForm.boss2SignLink = obj['boss2SignLink']
applicationForm.boss2SignHash = obj['boss2SignHash']
if obj['boss2Opinion'] is False:
applicationForm.researchResult = False
applicationForm.workingStatus = False
if applicationForm.boss1Opinion is not None and applicationForm.boss2Opinion is not None:
s = hashlib.sha256() # Get the hash algorithm.
s.update((applicationForm.boss1SignHash + applicationForm.boss2SignHash).encode("utf8")) # Hash the data.
applicationForm.checkHash = s.hexdigest() # Get he hash value.
bossSignOnChain(obj['id'], s.hexdigest())
applicationForm.save()
return HttpResponse("success")
class OfficeFile(models.Model):
#
dataTitle = models.CharField('檔案標題', max_length=50)
dataIntroduction = models.CharField('檔案簡介', max_length=255)
informationLink = models.CharField('檔案鏈接', max_length=255)
informationHash = models.CharField('檔案哈希值', max_length=64)
uploadTime = models.DateTimeField(verbose_name='上傳時間', auto_now_add=True)
downloadsNum = models.IntegerField('下載次數', default=0)
boss1SignLink = models.CharField('簽字圖片1下載鏈接', max_length=255, null=True)
boss1SignHash = models.CharField('簽字圖片1哈希值', max_length=64, null=True)
boss1Opinion = models.BooleanField('領導1意見', null=True)
boss2SignLink = models.CharField('簽字圖片2下載鏈接', max_length=255, null=True)
boss2SignHash = models.CharField('簽字圖片2哈希值', max_length=64, null=True)
boss2Opinion = models.BooleanField('領導2意見', null=True)
checkHash = models.CharField(verbose_name='審核結果哈希值', null=True, max_length=64)
reviewResult = models.BooleanField(verbose_name='審核結果', null=True)
researchResult = models.BooleanField(verbose_name='最終結果', null=True) # 最終意見opinion
workingStatus = models.BooleanField(verbose_name='作業已經完成', default=False, null=True)
class Meta:
verbose_name_plural = "辦公流程"
def __str__(self):
return self.dataTitle
4.后記
- 該專案已獲得2020-2021騰訊舉辦的高校微信小程式比賽華中賽區三等獎
- 所有相關代碼已經開源,運行有任何問題可以提issue,如專案對您有幫助,歡迎star支持!github地址點擊鏈接
- 本人關注前沿知識,熱衷于開源,獲得Fisco Bcos 2021年度貢獻MVP
- 目前在準備找Golong后端開發/區塊鏈開發相關實習,有一起的小伙伴可以滴滴.
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/396281.html
標籤:區塊鏈
