待過軍事訓練役的人肯定有著假日還要忙著回報休假狀況,而回報由於還是在line裡面,要馬就是要麻煩班頭整理,要馬就是在那邊卡來卡去,實在是麻煩至極,於是我趁中間的假日開始思考什麼樣的流程可以優化這個操作呢,最直觀想到的當然是linebot,讓每個人在回覆時下達指令,後端幫忙整理,最後再用特定指令叫出來,但實際寫出來給鄰兵試用後,發現全文字介面以及死板的指令操作造成相當大的負面回應,於是開始往具有圖形介面的googleSheet編輯,再利用lineBot調用googleSheetAPI的方案走。結果遇到由於共編手機板需要下載,以及看起來弱弱的、......等原因導致推廣失敗。最後決定使用網頁做為載體,提供單純的入口以及圖形化與特化的操作,終於成功推廣,以下進行技術說明
後端環境 | 前端環境 | 後端框架 | 前端框架 | 資料庫 | 資料庫介面 |
---|---|---|---|---|---|
node | line瀏覽器 | express | boostrap | mongodb | mongoClient(mongoDB原生) |
設定頁
班級專屬頁
設定班級成員範例圖
創建成功後取得專屬網址
複製網址
利用Visual Studio 2019 community 引用Express4框架
加載額外套件
架構預設設定變更
// view engine setup
app.engine('.html', require('ejs').__express)
app.set('views', path.join(__dirname, 'views')); //注意path要require一下
app.set('view engine', 'html')
'use strict';
var debug = require('debug');
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var routes = require('./routes/index');
var app = express();
// view engine setup
app.engine('.html', require('ejs').__express)
app.set('views', path.join(__dirname, 'views')); //注意path要require一下
app.set('view engine', 'html')
// uncomment after placing your favicon in /public
//app.use(favicon(__dirname + '/public/favicon.ico'));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', routes);
//// catch 404 and forward to error handler
//app.use(function (req, res, next) {
// var err = new Error('Not Found');
// err.status = 404;
// next(err);
//});
//// error handlers
//// development error handler
//// will print stacktrace
//if (app.get('env') === 'development') {
// app.use(function (err, req, res, next) {
// res.status(err.status || 500);
// res.render('error', {
// message: err.message,
// error: err
// });
// });
//}
//// production error handler
//// no stacktraces leaked to user
//app.use(function (err, req, res, next) {
// res.status(err.status || 500);
// res.render('error', {
// message: err.message,
// error: {}
// });
//});
app.set('port', process.env.PORT || 1451);
var server = app.listen(app.get('port'), function () {
debug('Express server listening on port ' + server.address().port);
console.log(process.env.PORT || 1451);
});
建立一個庫來儲存所有班級資料
每個班級有一個庫可以儲存每次回報的內容
//the URL that we can connect to this Web(door of this Web)
//進入班級專屬頁
router.get("/index/:token", function (req, res) {
res.render("index", { token: req.params.token });
});
//進入設定頁
router.get("/", function (req, res) {
res.render("set", {});
});
班級專屬頁為了使一句API提供給多個班級,使用了動態路由的技巧
2. 創建新班級
router.post("/buildClass", function (req, res) {
//name:collection name, data: the elements in collections, it is a string can be split by <-> and <_>
const name = req.body.name;
const data = req.body.data;//num_include-num_include-..........-num_include
MongoClient.connect(url, function (err, client) {
if (err) throw err;
IsExistCollection(name, client)
.then(bool => insertClassData(name, data, client))
.then(bool => res.end("success"))
.catch(bool => res.end("error"))
.finally(bool => client.close())
});
});
//check if this class have exist
function IsExistCollection(name, client) {
return new Promise((resolve, reject) => {
var db = client.db(dbUsers);
db.listCollections({ name: name })
.next(function (err, collinfo) {
err ? reject(false) : (collinfo ? reject(true) : resolve(false));
});
});
}
//record all users in the class
function insertClassData(name, data, client) {
return new Promise((resolve, reject) => {
var table = client.db(dbUsers).collection(name);
//split "data"
var numList = [];
var includeList = [];
data.split("-").forEach(element => {
numList.push(element.split("_")[0]);
includeList.push(element.split("_")[1]);
});
var jsonList = [];
for (var i in numList) {
var json = {};
json["num"] = numList[i];
json["include"] = includeList[i];
jsonList.push(json);
}
table.insertMany(jsonList, function (err, result) {
err ? reject(false) : resolve(true);
})
});
}
該API接受兩個參數,前者為獲取該創建班級的代號,後者為獲取班級所有使用者的號碼以及敘述(格式於上方註解中有說明)
做法是先檢查有沒有已創建該班級,可以藉由檢查armyUsers內有沒有collection名子為該班及代號來完成。
下一步是把第二個參數拆成jsonList插入資料庫,collection名為班級代號。
3. 個人進行回報
router.post("/send", function (req, res) {
const token = req.body.token;//each class have it's own db to save data,db's name is it's token
const when = req.body.when;
const who = req.body.who;
const what = req.body.what;
MongoClient.connect(url, function (err, client) {
if (err) throw err;
//console.log("Connected successfully to server");
const db = client.db(token);
const collection = db.collection(when);
// Insert some documents
collection.updateOne({ num: who }, { $set: { num: who, include: what } }, { upsert: true }, function (err, result) {
if (err) res.send("error");
else {
client.close();
res.send("success");
}
});
});
});
該API接受以下4個參數
做法是對名稱為'班級代號'的DB,名稱為'回報時間節點'的collection,更新一個document,內容含'回報者座號'及'回報內容'
而且要設為更新,讓使用者可以進行修改,upsert要設為true,這樣沒有得更新時才能改為插入。
4. 刷新班級看板
router.post("/refresh", function (req, res) {
const token = req.body.token;//each class have it's own db to save data,db's name is it's token
const when = req.body.when;
//console.log(req.body);
MongoClient.connect(url, function (err, client) {
if (err) throw "error";
getUsers(token, client)
.then(pkg => getResponse(pkg, token, when, client))
.then(re => res.send(re))
.catch(error => res.send(error))
.finally(re => client.close())
});
});
function getUsers(token, client) {
return new Promise((resolve, reject) => {
var table = client.db(dbUsers).collection(token);
table.find({}).sort({ _id : 1 }).toArray(function (err, result) {
err ? reject({ result: "connect error" }) : resolve(result);
})
});
}
function getResponse(pkg,token,when,client) {
return new Promise((resolve, reject) => {
var table = client.db(token).collection(when);
table.find({}).toArray(function (err, result) {
if (err)
reject("connect error");
else {
var json = {};
for (var i in result) json[result[i].num] = result[i].include;
var str = "";
for (var i in pkg)
str += "\n" + pkg[i].include + " : " + (json[pkg[i].num] != null ? json[pkg[i].num] : '<strong style="background-color: gray;">尚未回覆</strong>');
//console.log(reply(token, when, result.length, str));
//console.log("reply");
resolve(reply(token.split('~'), when, result.length, str));
}
})
});
}
function reply(token,when, length, str) {
return (
when +
"\n" + decodeURI(token[1]).toString() + "連訓員 第" + token[2] + "班\n今日看診人員:共0員\n發燒人員:共0員\n應到:" + token[3] + "員 \n實到:" +
length +
"員" +
str
);
}
這句API接受班級代號與時間節點兩個參數,
作法是先到armyUsers找到名稱為班級代號的collection取得班級所有設定資料
,再到名稱為'班級代號'的DB,名稱為'回報時間節點'的collection取得該班該時間節點的回報訊息
組合這兩個資訊進行排序再回傳結果字串。
'use strict';
var express = require('express');
var router = express.Router();
const MongoClient = require("mongodb").MongoClient;
// Connection URL
//local mongoDB URL
//const url = "mongodb://localhost:27017";
//cloud mongoDB URL
const url = "這裡要放雲端mongoDB的連結URL"
// Database Name
const dbUsers = "armyUsers";
//the URL that we can connect to this Web(door of this Web)
router.get("/index/:token", function (req, res) {
res.render("index", { token: req.params.token });
});
router.get("/", function (req, res) {
res.render("set", {});
});
/******************************************************
post: buildClass ,use it to build a collection which can let Web know who are in the class
db:armyUsers collection: (營)~(連)~(班)~(人數)~(第一時間)~(第二時間)~(第三時間)=>班級編號 element: num=>30, include=>31030 林小明
token: ex:3~步一~10~17~11~14~19 => (營)~(連)~(班)~(人數)~(第一時間)~(第二時間)~(第三時間)
*******************************************************/
router.post("/buildClass", function (req, res) {
//name:collection name, data: the elements in collections, it is a string can be split by <-> and <_>
const name = req.body.name;
const data = req.body.data;//num_include-num_include-..........-num_include
MongoClient.connect(url, function (err, client) {
if (err) throw err;
IsExistCollection(name, client)
.then(bool => insertClassData(name, data, client))
.then(bool => res.end("success"))
.catch(bool => res.end("error"))
});
});
//check if this class have exist
function IsExistCollection(name, client) {
return new Promise((resolve, reject) => {
var db = client.db(dbUsers);
db.listCollections({ name: name })
.next(function (err, collinfo) {
err ? reject(false) : (collinfo ? reject(true) : resolve(false));
});
});
}
//record all users in the class
function insertClassData(name, data, client) {
return new Promise((resolve, reject) => {
var table = client.db(dbUsers).collection(name);
//split "data"
var numList = [];
var includeList = [];
data.split("-").forEach(element => {
numList.push(element.split("_")[0]);
includeList.push(element.split("_")[1]);
});
var jsonList = [];
for (var i in numList) {
var json = {};
json["num"] = numList[i];
json["include"] = includeList[i];
jsonList.push(json);
}
table.insertMany(jsonList, function (err, result) {
err ? reject(false) : resolve(true);
})
});
}
/*****************************************
post:send Record => 'when'? 'who' do "what"
db:army collection:109/XX/XX XX點回報 element: num=>30,include=>1000在家睡覺
*****************************************/
router.post("/send", function (req, res) {
const token = req.body.token;//each class have it's own db to save data,db's name is it's token
const when = req.body.when;
const who = req.body.who;
const what = req.body.what;
MongoClient.connect(url, function (err, client) {
if (err) throw err;
//console.log("Connected successfully to server");
const db = client.db(token);
const collection = db.collection(when);
// Insert some documents
collection.updateOne({ num: who }, { $set: { num: who, include: what } }, { upsert: true }, function (err, result) {
if (err) res.send("error");
else {
client.close();
res.send("success");
}
});
});
});
/*****************************************
post:refresh => use token to find db, and use when to get goal, finally,return it
*****************************************/
//date + "\n一連訓員 第2班\n今日看診人員:共0員\n發燒人員:共0員\n應到:16員 \n實到:" + result.length + "員" + str
router.post("/refresh", function (req, res) {
const token = req.body.token;//each class have it's own db to save data,db's name is it's token
const when = req.body.when;
//console.log(req.body);
MongoClient.connect(url, function (err, client) {
if (err) throw "error";
getUsers(token, client)
.then(pkg => getResponse(pkg, token, when, client))
.then(re => res.send(re))
.catch(error => res.send(error));
});
});
function getUsers(token, client) {
return new Promise((resolve, reject) => {
var table = client.db(dbUsers).collection(token);
table.find({}).toArray(function (err, result) {
err ? reject({ result: "connect error" }) : resolve(result);
})
});
}
function getResponse(pkg,token,when,client) {
return new Promise((resolve, reject) => {
var table = client.db(token).collection(when);
table.find({}).toArray(function (err, result) {
if (err)
reject("connect error");
else {
var json = {};
for (var i in result) json[result[i].num] = result[i].include;
var str = "";
for (var i in pkg)
str += "\n" + pkg[i].include + " : " + (json[pkg[i].num] != null ? json[pkg[i].num] : '<strong style="background-color: gray;">尚未回覆</strong>');
//console.log(reply(token, when, result.length, str));
//console.log("reply");
resolve(reply(token.split('~'), when, result.length, str).replace());
}
})
});
}
function reply(token,when, length, str) {
return (
when +
"\n" + decodeURI(token[1]).toString() + "連訓員 第" + token[2] + "班\n今日看診人員:共0員\n發燒人員:共0員\n應到:" + token[3] + "員 \n實到:" +
length +
"員" +
str
);
}
module.exports = router;
因為較為簡單不進行一個一個的說明僅對重點做敘述
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
$.post("/buildClass", { name: encodeURI($("#newClassToken").val()), data: getData() }, function (result) {
由於'連'要支援中文,但之後藉由動態路由會出現再網址,要避免錯誤,於是利用encodeURI來進行轉換,於後端(index.js)最下面的reply函數中有decodeURI把其在輸出時轉換回中文。
在最下方的函數,進行頁面跳轉,要加入伺服器的網域名
<!DOCTYPE html>
<html>
<head>
<title>放假回報</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</head>
<body>
<header>
<div name="Title" class="jumbotron mb-0 ">
<div class="text-center align-self-center">
<h1>放假回報</h1>
</div>
</div>
</header>
<div class="container" style="font-family:Microsoft JhengHei;font-size:100%">
<div id="input" style="text-align:center">
<label>請輸入你的班級代號</label>
<input id="classToken" type="text" />
<input id="jumpPage" type="button" value="進入班級回報版" />
<br>
<hr />
<label>新增班級回報版</label>
<br>
<p>班級代號規則 -> ex:3~步一~10~17~11~14~19 => (營)~(連)~(班)~(人數)~(第一時間)~(第二時間)~(第三時間)。</p>
<input type="text" id="newClassToken" placeholder="請輸入班級代號"><br>
<label>請按照順序輸入班級內所有成員的座號(左)及資訊(右)</label><br>
<div style="display:none">01.<input id="n1" type="text" placeholder="ex:1"/><input type="text" id="s1" placeholder="ex:31001 王大明"><br></div>
<div style="display:none">02.<input id="n2" type="text" placeholder="ex:2" /><input type="text" id="s2" placeholder="ex:31002 王大明"><br></div>
<div style="display:none">03.<input id="n3" type="text" placeholder="ex:3" /><input type="text" id="s3" placeholder="ex:31003 王大明"><br></div>
<div style="display:none">04.<input id="n4" type="text" placeholder="ex:4" /><input type="text" id="s4" placeholder="ex:31004 王大明"><br></div>
<div style="display:none">05.<input id="n5" type="text" placeholder="ex:5" /><input type="text" id="s5" placeholder="ex:31005 王大明"><br></div>
<div style="display:none">06.<input id="n6" type="text" placeholder="ex:6" /><input type="text" id="s6" placeholder="ex:31006 王大明"><br></div>
<div style="display:none"> 07.<input id="n7" type="text" placeholder="ex:7" /><input type="text" id="s7" placeholder="ex:31007 王大明"><br></div>
<div style="display:none">08.<input id="n8" type="text" placeholder="ex:8" /><input type="text" id="s8" placeholder="ex:31008 王大明"><br></div>
<div style="display:none">09.<input id="n9" type="text" placeholder="ex:9" /><input type="text" id="s9" placeholder="ex:31009 王大明"><br></div>
<div style="display:none"> 10.<input id="n10" type="text" placeholder="ex:10" /><input type="text" id="s10" placeholder="ex:31010 王大明"><br></div>
<div style="display:none">11.<input id="n11" type="text" placeholder="ex:11" /><input type="text" id="s11" placeholder="ex:31011 王大明"><br></div>
<div style="display:none">12.<input id="n12" type="text" placeholder="ex:12" /><input type="text" id="s12" placeholder="ex:31012 王大明"><br></div>
<div style="display:none">13.<input id="n13" type="text" placeholder="ex:13" /><input type="text" id="s13" placeholder="ex:31013 王大明"><br></div>
<div style="display:none">14.<input id="n14" type="text" placeholder="ex:14" /><input type="text" id="s14" placeholder="ex:31014 王大明"><br></div>
<div style="display:none">15.<input id="n15" type="text" placeholder="ex:15" /><input type="text" id="s15" placeholder="ex:31015 王大明"><br></div>
<div style="display:none">16.<input id="n16" type="text" placeholder="ex:16" /><input type="text" id="s16" placeholder="ex:31016 王大明"><br></div>
<div style="display:none">17.<input id="n17" type="text" placeholder="ex:17" /><input type="text" id="s17" placeholder="ex:31017 王大明"><br></div>
<div style="display:none">18.<input id="n18" type="text" placeholder="ex:18" /><input type="text" id="s18" placeholder="ex:31018 王大明"><br></div>
<div style="display:none">19.<input id="n19" type="text" placeholder="ex:19" /><input type="text" id="s19" placeholder="ex:31019 王大明"><br></div>
<div style="display:none">20.<input id="n20" type="text" placeholder="ex:20" /><input type="text" id="s20" placeholder="ex:31020 王大明"><br></div>
<br>
<input id="PushUser" type="button" value="增加成員" />
<input id="PopUser" type="button" value="減少成員" />
<br /><br />
<button id="buildClass" class="btn btn-success">創建班級</button>
</div>
</div>
<br>
<br>
<script>
var count = 16;
function getData() {
var str = $("#n1").val() + "_" + $("#s1").val();
for (var i = 2; i <= count; i++)
str += "-" + $("#n" + i).val() + "_" + $("#s" + i).val();
return str;
}
$(document).ready(function () {
for (var i = count; i >= 1; i--)
$("#s" + i).parent("div").show();
$("#PushUser").click(function () {
if (count < 20)
$("#s" + (++count)).parent("div").show();
else
alert("20人為班級人數的極限");
});
$("#PopUser").click(function () {
if (count > 2)
$("#s" + (count--)).parent("div").hide();
else
alert("2人為班級人數的最小值");
});
$("#buildClass").click(function () {
if ($("#newClassToken").val() != null) {
$.post("/buildClass", { name: encodeURI($("#newClassToken").val()), data: getData() }, function (result) {
alert(result);
})
} else
alert("請填入課程代號");
});
$("#jumpPage").click(function () {
location.href = "這裡輸入伺服器網域名" +"/index/" + $("#classToken").val();
//location.href = "http://127.0.0.1:1337/index/" + encodeURI($("#classToken").val());
});
});
</script>
</body>
</html>
function Copy(str) {
//創建一個textarea標籤,由於該網頁API操作僅可對此標籤進行
var clip_area = document.createElement('textarea');
//把內容放入標籤
clip_area.textContent = str;
//新增標籤至實際網頁
document.body.appendChild(clip_area);
//選取該標籤
clip_area.select();
//執行複製指令
document.execCommand('copy');
//移除標籤
clip_area.remove();
alert("已複製好,可黏貼");
}
<!DOCTYPE html>
<html>
<head>
<title>放假回報</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</head>
<body>
<header>
<div name="Title" class="jumbotron mb-0 ">
<div class="text-center align-self-center">
<h1>放假回報</h1>
</div>
</div>
</header>
<div class="container" style="font-family:Microsoft JhengHei;font-size:100%">
<div id="input" style="text-align:center">
<label>請輸入你的學號末兩碼</label>
<input type="number" id="number">
<br>
<label>回報時間</label>
<select id="select">
<option id="option1" value=""></option>
<option id="option2" value=""></option>
<option id="option3" value=""></option>
</select>
<br>
<input type="text" id="text" placeholder="請輸入回報內容">
<button id="send">傳送</button>
<pre id="include"></pre>
<button id="duplicate">複製</button>
<button id="refresh">刷新</button>
</div>
</div>
<script>
$(document).ready(function () {
var token = encodeURI('<%=token%>');
//console.log(token);
var dt = new Date();
var now = dt.getHours();
var select = document.getElementById("select");
const times = token.split('~');
$("#option1").val(times[4] + "時回報");
$("#option2").val(times[5] + "時回報");
$("#option3").val(times[6] + "時回報");
$("#option1").html(times[4] + "時回報");
$("#option2").html(times[5] + "時回報");
$("#option3").html(times[6] + "時回報");
if (now <= parseInt(times[4]) + 1)
select.options[0].selected = true;
else if (now <= parseInt(times[5]) + 1)
select.options[1].selected = true;
else
select.options[2].selected = true;
if (localStorage.getItem("num"))
$("#number").val(localStorage.getItem("num"));
$.post("/refresh", { token: token, when: (parseInt(dt.getFullYear()) - 1911).toString() + "/" + (parseInt(dt.getMonth()) + 1).toString() + "/" + dt.getDate().toString() + " " + $("select").val() }, function (result) {
$("pre").html(result);
})
$("#send").click(function () {
if (parseInt($("#number").val()) >= 0) {
$.post("/send", { token: token, when: (parseInt(dt.getFullYear()) - 1911).toString() + "/" + (parseInt(dt.getMonth()) + 1).toString() + "/" + dt.getDate().toString() + " " + $("select").val(), who: parseInt($("#number").val()).toString(), what: $("#text").val() }, function (result) {
$.post("/refresh", { token: token, when: (parseInt(dt.getFullYear()) - 1911).toString() + "/" + (parseInt(dt.getMonth()) + 1).toString() + "/" + dt.getDate().toString() + " " + $("select").val() }, function (result) {
$("pre").html(result);
$("#text").val('');
})
})
localStorage.setItem("num", $("#number").val().toString());
} else {
alert('請檢查你的學號是否輸入正確');
}
});
$("#refresh").click(function () {
$.post("/refresh", { token: token, when: (parseInt(dt.getFullYear()) - 1911).toString() + "/" + (parseInt(dt.getMonth()) + 1).toString() + "/" + dt.getDate().toString() + " " + $("select").val() }, function (result) {
$("pre").html(result);
})
});
function Copy(str) {
var clip_area = document.createElement('textarea');
clip_area.textContent = str;
document.body.appendChild(clip_area);
clip_area.select();
document.execCommand('copy');
clip_area.remove();
alert("已複製好,可黏貼");
}
$("#duplicate").click(function () {
Copy($("pre").html().replace(/<[^>]+>/g, ""));
})
$("select").change(function () {
$.post("/refresh", { token: token, when: (parseInt(dt.getFullYear()) - 1911).toString() + "/" + (parseInt(dt.getMonth()) + 1).toString() + "/" + dt.getDate().toString() + " " + $("select").val() }, function (result) {
$("pre").html(result);
})
});
})
</script>
</body>
</html>
若要實際發布此網站,在引用架構,編寫所有檔案後還不夠還有資料庫與伺服器的問題,不過由於不是本篇重點,所以我將簡單帶過。
資料庫我是使用MongoDB Altis,帳號申辦簡單,以本專案來說也有相當夠用的免費存儲空間。
我個人用過GCP(google clooud platform)在剛使用有一定額度的免費,但個人經驗一下就用完了,而且使用複雜度相對較高。
其他就是各種雲伺服器。
不過以我個人而言最喜歡使用的方案是安裝在個人可連接外網的機器裡,用pm2發布。
git連結:
https://github.com/leon123858/soldiers_response_system/tree/main/Web/Web
若想直接使用,記得在index.js加入資料庫連結網址,在set.html加入伺服器網域才可以順利運作,在上方內容源碼區都有用中文補在該插入的地方。
此外
若講述不好歡迎建議或補充,
若講述有誤歡迎指正。