mirror of
https://github.com/kuaifan/dootask.git
synced 2026-01-13 01:28:11 +00:00
feat: 签到功能
This commit is contained in:
parent
2c7f4837d5
commit
e3cdb20579
@ -2,9 +2,10 @@
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Models\User;
|
||||
use App\Models\UserCheckin;
|
||||
use App\Models\UserCheckinRecord;
|
||||
use App\Module\Base;
|
||||
use Carbon\Carbon;
|
||||
use Request;
|
||||
@ -16,127 +17,95 @@ use Request;
|
||||
*/
|
||||
class PublicController extends AbstractController
|
||||
{
|
||||
const appid = "10001";
|
||||
const appkey = "TWVCVBJSiCjAFOPpFVdkpQCMWDw66EUY";
|
||||
|
||||
/**
|
||||
* 验证签名
|
||||
* @return void
|
||||
* 签到 - 路由器(openwrt)功能安装脚本
|
||||
*
|
||||
* @apiParam {String} key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function _sign()
|
||||
public function checkin__install()
|
||||
{
|
||||
$query = Request::query();
|
||||
$query['sign'] = Request::header('sign') ?: Request::input('sign');
|
||||
// 检查必要参数
|
||||
if ($query['appid'] != self::appid) {
|
||||
throw new ApiException('appid is error');
|
||||
$key = trim(Request::input('key'));
|
||||
//
|
||||
$setting = Base::setting('checkinSetting');
|
||||
if ($setting['wifi'] !== 'open') {
|
||||
return <<<EOF
|
||||
#!/bin/sh
|
||||
echo "function off"
|
||||
EOF;
|
||||
}
|
||||
foreach (['appid', 'ver', 'ts', 'nonce'] as $key) {
|
||||
if (!isset($query[$key])) {
|
||||
throw new ApiException($key . ' parameter is empty');
|
||||
}
|
||||
}
|
||||
if (intval($query['ts']) + 300 < time()) {
|
||||
throw new ApiException('ts expired');
|
||||
}
|
||||
// 验证签名
|
||||
ksort($query);
|
||||
$string = "";
|
||||
foreach ($query as $k => $v) {
|
||||
if ($v != '' && $k != 'sign') {
|
||||
$string .= $k . "=" . $v . "&";
|
||||
}
|
||||
}
|
||||
$sign = md5($string . self::appkey);
|
||||
if ($sign != $query['sign']) {
|
||||
throw new ApiException('sign is error');
|
||||
if ($key != $setting['key']) {
|
||||
return <<<EOF
|
||||
#!/bin/sh
|
||||
echo "key error"
|
||||
EOF;
|
||||
}
|
||||
//
|
||||
$reportUrl = Base::fillUrl("api/public/checkin/report");
|
||||
return <<<EOE
|
||||
#!/bin/sh
|
||||
echo 'installing...'
|
||||
|
||||
cat > /etc/init.d/dootask-checkin-report <<EOF
|
||||
#!/bin/sh
|
||||
mac=\\\$(awk 'NR!=1&&\\\$3=="0x2" {print \\\$4}' /proc/net/arp | tr "\\n" ",")
|
||||
tmp='{"key":"{$setting['key']}","mac":"'\\\${mac}'","time":"'\\\$(date +%s)'"}'
|
||||
curl -4 -X POST "{$reportUrl}" -H "Content-Type: application/json" -d \\\${tmp}
|
||||
EOF
|
||||
|
||||
chmod +x /etc/init.d/dootask-checkin-report
|
||||
crontab -l >/tmp/cronbak
|
||||
sed -i '/\/etc\/init.d\/dootask-checkin-report/d' /tmp/cronbak
|
||||
sed -i '/^$/d' /tmp/cronbak
|
||||
echo "* * * * * sh /etc/init.d/dootask-checkin-report" >>/tmp/cronbak
|
||||
crontab /tmp/cronbak
|
||||
rm -f /tmp/cronbak
|
||||
/etc/init.d/cron enable
|
||||
/etc/init.d/cron restart
|
||||
|
||||
echo 'installed'
|
||||
EOE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/public/attendance/portraitlist 01. 【考勤】人员头像数据
|
||||
* {post} 签到 - 路由器(openwrt)上报
|
||||
*
|
||||
* @apiDescription 需要签名
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup public
|
||||
* @apiName attendance__portraitlist
|
||||
* @apiParam {String} key
|
||||
* @apiParam {String} mac 使用逗号分割多个
|
||||
* @apiParam {String} time
|
||||
*
|
||||
* @apiParam {String} last_at 最后获取时间(格式示例:2022-01-01 12:50:01)
|
||||
*
|
||||
* @apiParam {String} appid 唯一身份ID,跟签名appkey配合使用
|
||||
* @apiParam {String} ver 版本号,如:1.0
|
||||
* @apiParam {Number} ts 10位数时间戳(有效时间300秒)
|
||||
* @apiParam {String} nonce 随机字符串
|
||||
* @apiParam {String} sign 签名字符串=md5(query_key1=query_val1&query_key2=query_val2...&appkey)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
* @return string
|
||||
*/
|
||||
public function attendance__portraitlist()
|
||||
public function checkin__report()
|
||||
{
|
||||
$this->_sign();
|
||||
//
|
||||
$last_at = Request::input('last_at');
|
||||
//
|
||||
$builder = User::where('userimg', '!=', '')->whereNull('disable_at');
|
||||
if (strtotime($last_at)) {
|
||||
$builder->where('updated_at', '>', Carbon::parse($last_at));
|
||||
}
|
||||
$list = $builder->orderBy('updated_at')->take(50)->get();
|
||||
//
|
||||
$array = [];
|
||||
foreach ($list as $item) {
|
||||
$array[] = [
|
||||
'userid' => $item->userid,
|
||||
'userimg' => $item->userimg,
|
||||
'updated_at' => $item->updated_at,
|
||||
];
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('success', $array);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/public/attendance/update 02. 【考勤】上报考勤数据
|
||||
*
|
||||
* @apiDescription 需要签名
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup public
|
||||
* @apiName attendance__update
|
||||
*
|
||||
* @apiParam {Number} userid 会员ID
|
||||
* @apiParam {Number} time 时间数据(10位数时间戳)
|
||||
*
|
||||
* @apiParam {String} appid 唯一身份ID,跟签名appkey配合使用
|
||||
* @apiParam {String} ver 版本号,如:1.0
|
||||
* @apiParam {Number} ts 10位数时间戳(有效时间300秒)
|
||||
* @apiParam {String} nonce 随机字符串
|
||||
* @apiParam {String} sign 签名字符串=md5(query_key1=query_val1&query_key2=query_val2...&appkey)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function attendance__update()
|
||||
{
|
||||
$this->_sign();
|
||||
//
|
||||
$userid = intval(Request::input('userid'));
|
||||
$key = trim(Request::input('key'));
|
||||
$mac = trim(Request::input('mac'));
|
||||
$time = intval(Request::input('time'));
|
||||
//
|
||||
$user = User::whereUserid($userid)->first();
|
||||
if (empty($user)) {
|
||||
return Base::retError('user not exist');
|
||||
$setting = Base::setting('checkinSetting');
|
||||
if ($setting['wifi'] !== 'open') {
|
||||
return 'function off';
|
||||
}
|
||||
if ($key != $setting['key']) {
|
||||
return 'key error';
|
||||
}
|
||||
// todo 保存到考勤数据库
|
||||
info([
|
||||
'userid' => $user->userid,
|
||||
'input' => Request::input(),
|
||||
'time' => $time,
|
||||
'at' => Carbon::now()->toDateTimeString(),
|
||||
]);
|
||||
//
|
||||
return Base::retSuccess('success');
|
||||
$macs = explode(",", $mac);
|
||||
foreach ($macs as $item) {
|
||||
$item = strtoupper($item);
|
||||
if (empty($item) || !preg_match("/^[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}$/", $item)) {
|
||||
continue;
|
||||
}
|
||||
$userCheckin = UserCheckin::whereMac($item)->first();
|
||||
if ($userCheckin) {
|
||||
UserCheckinRecord::createInstance([
|
||||
'userid' => $userCheckin->userid,
|
||||
'mac' => $userCheckin->mac,
|
||||
'time' => $time,
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
return 'success';
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,11 +4,17 @@ namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use App\Models\UserCheckinRecord;
|
||||
use App\Module\Base;
|
||||
use App\Module\BillExport;
|
||||
use App\Module\BillMultipleExport;
|
||||
use Arr;
|
||||
use Carbon\Carbon;
|
||||
use Guanguans\Notify\Factory;
|
||||
use Guanguans\Notify\Messages\EmailMessage;
|
||||
use Madzipper;
|
||||
use Request;
|
||||
use Session;
|
||||
|
||||
/**
|
||||
* @apiDefine system
|
||||
@ -208,6 +214,57 @@ class SystemController extends AbstractController
|
||||
return Base::retSuccess('success', $setting ?: json_decode('{}'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/checkin 03. 获取签到设置、保存签到设置(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
* @apiName setting__checkin
|
||||
*
|
||||
* @apiParam {String} type
|
||||
* - get: 获取(默认)
|
||||
* - save: 保存设置(参数:['wifi', 'key'])
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function setting__checkin()
|
||||
{
|
||||
User::auth('admin');
|
||||
//
|
||||
$type = trim(Request::input('type'));
|
||||
if ($type == 'save') {
|
||||
if (env("SYSTEM_SETTING") == 'disabled') {
|
||||
return Base::retError('当前环境禁止修改');
|
||||
}
|
||||
$all = Request::input();
|
||||
foreach ($all as $key => $value) {
|
||||
if (!in_array($key, [
|
||||
'wifi',
|
||||
'key',
|
||||
])) {
|
||||
unset($all[$key]);
|
||||
}
|
||||
}
|
||||
if ($all['wifi'] === 'close') {
|
||||
$all['key'] = md5(Base::generatePassword(32));
|
||||
}
|
||||
$setting = Base::setting('checkinSetting', Base::newTrim($all));
|
||||
} else {
|
||||
$setting = Base::setting('checkinSetting');
|
||||
}
|
||||
//
|
||||
if (empty($setting['key'])) {
|
||||
$setting['key'] = md5(Base::generatePassword(32));
|
||||
Base::setting('checkinSetting', $setting);
|
||||
}
|
||||
//
|
||||
$setting['wifi'] = $setting['wifi'] ?: 'close';
|
||||
$setting['cmd'] = "curl -sSL '" . Base::fillUrl("api/public/checkin/install?key={$setting['key']}") . "' | sh";
|
||||
//
|
||||
return Base::retSuccess('success', $setting ?: json_decode('{}'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/apppush 04. 获取APP推送设置、保存APP推送设置(限管理员)
|
||||
*
|
||||
@ -742,6 +799,171 @@ class SystemController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/checkin/export 17. 导出签到数据(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
* @apiName checkin__export
|
||||
*
|
||||
* @apiParam {Array} [userid] 指定会员,如:[1, 2]
|
||||
* @apiParam {Array} [date] 指定日期范围,如:['2020-12-12', '2020-12-30']
|
||||
* @apiParam {Array} [time] 指定时间范围,如:['09:00', '18:00']
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function checkin__export()
|
||||
{
|
||||
User::auth('admin');
|
||||
//
|
||||
$userid = Base::arrayRetainInt(Request::input('userid'), true);
|
||||
$date = Request::input('date');
|
||||
$time = Request::input('time');
|
||||
//
|
||||
if (empty($userid) || empty($date) || empty($time)) {
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
if (count($userid) > 20) {
|
||||
return Base::retError('导出成员限制最多20个');
|
||||
}
|
||||
if (!(is_array($date) && Base::isDate($date[0]) && Base::isDate($date[1]))) {
|
||||
return Base::retError('日期选择错误');
|
||||
}
|
||||
if (Carbon::parse($date[1])->timestamp - Carbon::parse($date[0])->timestamp > 35 * 86400) {
|
||||
return Base::retError('日期范围限制最大35天');
|
||||
}
|
||||
if (!(is_array($time) && Base::isTime($time[0]) && Base::isTime($time[1]))) {
|
||||
return Base::retError('时间选择错误');
|
||||
}
|
||||
//
|
||||
$secondStart = strtotime("2000-01-01 {$time[0]}") - strtotime("2000-01-01 00:00:00");
|
||||
$secondEnd = strtotime("2000-01-01 {$time[1]}") - strtotime("2000-01-01 00:00:00");
|
||||
//
|
||||
$headings = [];
|
||||
$headings[] = '成员ID';
|
||||
$headings[] = '成员名称';
|
||||
$headings[] = '成员邮箱';
|
||||
$headings[] = '签到日期';
|
||||
$headings[] = '签到时间1';
|
||||
$headings[] = '签到时间2';
|
||||
//
|
||||
$sheets = [];
|
||||
$start = Carbon::parse($date[0])->startOfDay();
|
||||
$end = Carbon::parse($date[1])->endOfDay();
|
||||
$users = User::whereIn('userid', $userid)->take(20)->get();
|
||||
/** @var User $user */
|
||||
foreach ($users as $user) {
|
||||
$records = UserCheckinRecord::whereUserid($user->userid)->whereBetween("created_at", [$start, $end])->orderBy('id')->get();
|
||||
//
|
||||
$datas = [];
|
||||
$styles = [];
|
||||
$startT = $start->timestamp;
|
||||
$endT = $end->timestamp;
|
||||
$index = 1;
|
||||
while ($startT < $endT) {
|
||||
$index++;
|
||||
$first = $records->whereBetween("created_at", [Carbon::parse($startT), Carbon::parse($startT + $secondStart)])->first();
|
||||
$last = $records->whereBetween("created_at", [Carbon::parse($startT + $secondEnd), Carbon::parse($startT + 86400)])->last();
|
||||
$first = $first ? Carbon::parse($first->created_at)->timestamp : 0;
|
||||
$last = $last ? Carbon::parse($last->created_at)->timestamp : 0;
|
||||
if (empty($first) || $first > $startT + $secondStart) {
|
||||
$styles["E{$index}"] = [
|
||||
'font' => [
|
||||
'color' => [
|
||||
'rgb' => 'ff0000'
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
if (empty($last) || $last < $startT + $secondEnd) {
|
||||
$styles["F{$index}"] = [
|
||||
'font' => [
|
||||
'color' => [
|
||||
'rgb' => 'ff0000'
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
$datas[] = [
|
||||
$user->userid,
|
||||
$user->nickname,
|
||||
$user->email,
|
||||
date("Y-m-d", $startT),
|
||||
$first ? date("H:i", $first) : '-',
|
||||
$last ? date("H:i", $last) : '-',
|
||||
];
|
||||
$startT += 86400;
|
||||
}
|
||||
$sheets[] = BillExport::create()->setTitle($user->nickname)->setHeadings($headings)->setData($datas)->setStyles($styles);
|
||||
}
|
||||
if (empty($sheets)) {
|
||||
return Base::retError('没有任何数据');
|
||||
}
|
||||
//
|
||||
$fileName = $users[0]->nickname;
|
||||
if (count($users) > 1) {
|
||||
$fileName .= "等" . count($userid) . "位成员";
|
||||
}
|
||||
$fileName .= '签到记录_' . Base::time() . '.xls';
|
||||
$filePath = "temp/checkin/export/" . date("Ym", Base::time());
|
||||
$export = new BillMultipleExport($sheets);
|
||||
$res = $export->store($filePath . "/" . $fileName);
|
||||
if ($res != 1) {
|
||||
return Base::retError('导出失败,' . $fileName . '!');
|
||||
}
|
||||
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
|
||||
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xls') . ".zip";
|
||||
$zipPath = storage_path($zipFile);
|
||||
if (file_exists($zipPath)) {
|
||||
Base::deleteDirAndFile($zipPath, true);
|
||||
}
|
||||
try {
|
||||
Madzipper::make($zipPath)->add($xlsPath)->close();
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
//
|
||||
if (file_exists($zipPath)) {
|
||||
$base64 = base64_encode(Base::array2string([
|
||||
'file' => $zipFile,
|
||||
]));
|
||||
Session::put('checkin::export:userid', $user->userid);
|
||||
return Base::retSuccess('success', [
|
||||
'size' => Base::twoFloat(filesize($zipPath) / 1024, true),
|
||||
'url' => Base::fillUrl('api/system/checkin/down?key=' . urlencode($base64)),
|
||||
]);
|
||||
} else {
|
||||
return Base::retError('打包失败,请稍后再试...');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/checkin/down 17. 下载导出的签到数据
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
* @apiName checkin__down
|
||||
*
|
||||
* @apiParam {String} key 通过export接口得到的下载钥匙
|
||||
*
|
||||
* @apiSuccess {File} data 返回数据(直接下载文件)
|
||||
*/
|
||||
public function checkin__down()
|
||||
{
|
||||
$userid = Session::get('checkin::export:userid');
|
||||
if (empty($userid)) {
|
||||
return Base::ajaxError("请求已过期,请重新导出!", [], 0, 502);
|
||||
}
|
||||
//
|
||||
$array = Base::string2array(base64_decode(urldecode(Request::input('key'))));
|
||||
$file = $array['file'];
|
||||
if (empty($file) || !file_exists(storage_path($file))) {
|
||||
return Base::ajaxError("文件不存在!", [], 0, 502);
|
||||
}
|
||||
return response()->download(storage_path($file));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/version 18. 获取版本号
|
||||
*
|
||||
|
||||
@ -7,6 +7,7 @@ use App\Models\Meeting;
|
||||
use App\Models\Project;
|
||||
use App\Models\UmengAlias;
|
||||
use App\Models\User;
|
||||
use App\Models\UserCheckin;
|
||||
use App\Models\UserDelete;
|
||||
use App\Models\UserDepartment;
|
||||
use App\Models\UserEmailVerification;
|
||||
@ -1287,4 +1288,87 @@ class UsersController extends AbstractController
|
||||
//
|
||||
return Base::retSuccess('删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/checkin/get 22. 获取签到设置
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName checkin__get
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function checkin__get()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$list = UserCheckin::whereUserid($user->userid)->orderBy('id')->get();
|
||||
//
|
||||
return Base::retSuccess('success', $list);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/users/checkin/save 22. 保存签到设置
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName checkin__save
|
||||
*
|
||||
* @apiParam {Array} list 优先级数据,格式:[{mac,remark}]
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function checkin__save()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
if (Base::settingFind('checkinSetting', 'wifi') !== 'open') {
|
||||
return Base::retError('此功能未开启,请联系管理员开启');
|
||||
}
|
||||
//
|
||||
$list = Base::getPostValue('list');
|
||||
$array = [];
|
||||
if (empty($list) || !is_array($list)) {
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
foreach ($list AS $item) {
|
||||
$item = Base::newTrim($item);
|
||||
if (empty($item['mac']) || !preg_match("/^[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}$/", $item['mac'])) {
|
||||
continue;
|
||||
}
|
||||
$array[] = [
|
||||
'mac' => strtoupper($item['mac']),
|
||||
'remark' => substr($item['remark'], 0, 50),
|
||||
];
|
||||
}
|
||||
if (count($array) > 3) {
|
||||
return Base::retError('最多只能添加3个MAC地址');
|
||||
}
|
||||
//
|
||||
return AbstractModel::transaction(function() use ($array, $user) {
|
||||
$ids = [];
|
||||
$list = [];
|
||||
foreach ($array as $item) {
|
||||
$row = UserCheckin::updateInsert([
|
||||
'userid' => $user->userid,
|
||||
'mac' => $item['mac'],
|
||||
], [
|
||||
'remark' => $item['remark'],
|
||||
]);
|
||||
if ($row) {
|
||||
$ids[] = $row->id;
|
||||
$list[] = $row;
|
||||
}
|
||||
}
|
||||
UserCheckin::whereUserid($user->userid)->whereNotIn('id', $ids)->delete();
|
||||
//
|
||||
return Base::retSuccess('success', $list);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,6 +54,12 @@ class VerifyCsrfToken extends Middleware
|
||||
// 保存汇报
|
||||
'api/report/store/',
|
||||
|
||||
// 签到设置
|
||||
'api/users/checkin/save/',
|
||||
|
||||
// 签到上报
|
||||
'api/public/checkin/report/',
|
||||
|
||||
// 发布桌面端
|
||||
'desktop/publish/',
|
||||
];
|
||||
|
||||
31
app/Models/UserCheckin.php
Normal file
31
app/Models/UserCheckin.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
|
||||
/**
|
||||
* App\Models\UserCheckin
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $userid 会员id
|
||||
* @property string|null $mac MAC地址
|
||||
* @property string|null $remark 备注
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckin newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckin newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckin query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckin whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckin whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckin whereMac($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckin whereRemark($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckin whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckin whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UserCheckin extends AbstractModel
|
||||
{
|
||||
|
||||
}
|
||||
31
app/Models/UserCheckinRecord.php
Normal file
31
app/Models/UserCheckinRecord.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
|
||||
/**
|
||||
* App\Models\UserCheckinRecord
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $userid 会员id
|
||||
* @property string|null $mac MAC地址
|
||||
* @property int|null $time 上报的时间戳
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereMac($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereTime($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UserCheckinRecord extends AbstractModel
|
||||
{
|
||||
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateUserCheckinsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('user_checkins', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('userid')->nullable()->default(0)->comment('会员id');
|
||||
$table->string('mac', 100)->nullable()->default('')->comment('MAC地址');
|
||||
$table->string('remark', 100)->nullable()->default('')->comment('备注');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('user_checkins');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateUserCheckinRecordsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('user_checkin_records', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('userid')->nullable()->default(0)->comment('会员id');
|
||||
$table->string('mac', 100)->nullable()->default('')->comment('MAC地址');
|
||||
$table->bigInteger('time')->nullable()->default(0)->comment('上报的时间戳');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('user_checkin_records');
|
||||
}
|
||||
}
|
||||
113
resources/assets/js/pages/manage/setting/checkin.vue
Normal file
113
resources/assets/js/pages/manage/setting/checkin.vue
Normal file
@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="setting-item submit">
|
||||
<Form ref="formData" label-width="auto" @submit.native.prevent>
|
||||
<Alert show-icon style="margin-bottom:18px">
|
||||
{{$L('手机连接上指定WIFI后自动签到。')}}
|
||||
</Alert>
|
||||
<Row class="setting-template">
|
||||
<Col span="12">{{$L('MAC地址')}}</Col>
|
||||
<Col span="12">{{$L('备注')}}</Col>
|
||||
</Row>
|
||||
<Row v-for="(item, key) in formData" :key="key" class="setting-template">
|
||||
<Col span="12">
|
||||
<Input
|
||||
v-model="item.mac"
|
||||
:maxlength="20"
|
||||
:placeholder="$L('请输入MAC地址')"
|
||||
clearable
|
||||
@on-clear="delDatum(key)"/>
|
||||
</Col>
|
||||
<Col span="12">
|
||||
<Input v-model="item.remark" :maxlength="100" :placeholder="$L('备注')"/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Button type="default" icon="md-add" @click="addDatum">{{$L('添加地址')}}</Button>
|
||||
</Form>
|
||||
<div class="setting-footer">
|
||||
<Button :loading="loadIng > 0" type="primary" @click="submitForm">{{$L('提交')}}</Button>
|
||||
<Button :loading="loadIng > 0" @click="resetForm" style="margin-left: 8px">{{$L('重置')}}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loadIng: 0,
|
||||
|
||||
formData: [],
|
||||
|
||||
nullDatum: {
|
||||
'mac': '',
|
||||
'remark': '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.initData();
|
||||
},
|
||||
|
||||
methods: {
|
||||
initData() {
|
||||
this.loadIng++;
|
||||
this.$store.dispatch("call", {
|
||||
url: 'users/checkin/get',
|
||||
}).then(({data}) => {
|
||||
this.formData = data.length > 0 ? data : [$A.cloneJSON(this.nullDatum)];
|
||||
this.formData_bak = $A.cloneJSON(this.formData);
|
||||
}).catch(({msg}) => {
|
||||
$A.modalError(msg);
|
||||
}).finally(_ => {
|
||||
this.loadIng--;
|
||||
});
|
||||
},
|
||||
|
||||
submitForm() {
|
||||
this.$refs.formData.validate((valid) => {
|
||||
if (valid) {
|
||||
const list = this.formData
|
||||
.filter(item => /^[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}$/.test(item.mac.trim()))
|
||||
.map(item => {
|
||||
return {
|
||||
mac: item.mac.trim(),
|
||||
remark: item.remark.trim()
|
||||
}
|
||||
});
|
||||
//
|
||||
this.loadIng++;
|
||||
this.$store.dispatch("call", {
|
||||
url: 'users/checkin/save',
|
||||
data: {list},
|
||||
method: 'post',
|
||||
}).then(({data}) => {
|
||||
this.formData = data;
|
||||
this.formData_bak = $A.cloneJSON(this.formData);
|
||||
$A.messageSuccess('修改成功');
|
||||
}).catch(({msg}) => {
|
||||
$A.modalError(msg);
|
||||
}).finally(_ => {
|
||||
this.loadIng--;
|
||||
});
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
resetForm() {
|
||||
this.formData = $A.cloneJSON(this.formData_bak);
|
||||
},
|
||||
|
||||
addDatum() {
|
||||
this.formData.push($A.cloneJSON(this.nullDatum));
|
||||
},
|
||||
|
||||
delDatum(key) {
|
||||
this.formData.splice(key, 1);
|
||||
if (this.formData.length === 0) {
|
||||
this.addDatum();
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div class="setting-component-item">
|
||||
<Form ref="formData" :model="formData" :rules="ruleData" label-width="auto" @submit.native.prevent>
|
||||
<div class="block-setting-box">
|
||||
<h3>{{ $L('WIFI签到') }}</h3>
|
||||
<FormItem :label="$L('功能开启')" prop="wifi">
|
||||
<RadioGroup v-model="formData.wifi">
|
||||
<Radio label="open">{{ $L('开启') }}</Radio>
|
||||
<Radio label="close">{{ $L('关闭') }}</Radio>
|
||||
</RadioGroup>
|
||||
<div class="export-data" @click="exportShow=true">{{$L('导出签到数据')}}</div>
|
||||
</FormItem>
|
||||
<template v-if="formData.wifi === 'open'">
|
||||
<FormItem :label="$L('功能说明')" prop="explain">
|
||||
<p>1. {{$L('此功能仅支持手机客户端使用。')}}</p>
|
||||
<p>2. {{$L('手机连接上指定路由器WIFI后自动签到。')}}{{$L('(注:理论上不限制连接方式)')}}</p>
|
||||
<p>3. {{$L('签到延迟时长为±1分钟。')}}</p>
|
||||
</FormItem>
|
||||
<FormItem :label="$L('安装说明')" prop="install">
|
||||
<p>1. {{$L('此功能仅支持Openwrt系统的路由器。')}}</p>
|
||||
<p>2. {{$L('关闭签到功能再开启需要重新安装。')}}</p>
|
||||
<p>3. {{$L('进入路由器终端执行以下命令即可完成安装:')}}</p>
|
||||
<Input ref="cmd" @on-focus="clickCmd" style="margin-top:6px" type="textarea" readonly :value="formData.cmd"/>
|
||||
</FormItem>
|
||||
</template>
|
||||
</div>
|
||||
</Form>
|
||||
<div class="setting-footer">
|
||||
<Button :loading="loadIng > 0" type="primary" @click="submitForm">{{ $L('提交') }}</Button>
|
||||
<Button :loading="loadIng > 0" @click="resetForm" style="margin-left: 8px">{{ $L('重置') }}</Button>
|
||||
</div>
|
||||
|
||||
<!--导出签到数据-->
|
||||
<Modal
|
||||
v-model="exportShow"
|
||||
:title="$L('导出签到数据')"
|
||||
:mask-closable="false">
|
||||
<Form ref="export" :model="exportData" label-width="auto" @submit.native.prevent>
|
||||
<FormItem :label="$L('导出成员')">
|
||||
<UserInput v-model="exportData.userid" :multiple-max="20" :placeholder="$L('请选择成员')"/>
|
||||
</FormItem>
|
||||
<FormItem :label="$L('签到日期')">
|
||||
<DatePicker
|
||||
v-model="exportData.date"
|
||||
type="daterange"
|
||||
format="yyyy/MM/dd"
|
||||
style="width:100%"
|
||||
:placeholder="$L('请选择签到日期')"/>
|
||||
<div class="page-setting-checkin-export-common">
|
||||
{{$L('快捷选择')}}:
|
||||
<em @click="exportData.date=dateShortcuts('prev')">上个月</em>
|
||||
<em @click="exportData.date=dateShortcuts('this')">这个月</em>
|
||||
</div>
|
||||
</FormItem>
|
||||
<FormItem :label="$L('签到时间')">
|
||||
<TimePicker
|
||||
v-model="exportData.time"
|
||||
type="timerange"
|
||||
format="HH:mm"
|
||||
style="width:100%"
|
||||
:placeholder="$L('请选择签到时间')"/>
|
||||
<div class="page-setting-checkin-export-common">
|
||||
{{$L('快捷选择')}}:
|
||||
<em @click="exportData.time=['8:30', '18:00']">8:30-18:00</em>
|
||||
<em @click="exportData.time=['9:00', '18:00']">9:00-18:00</em>
|
||||
<em @click="exportData.time=['9:30', '18:00']">9:30-18:30</em>
|
||||
</div>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<div slot="footer" class="adaption">
|
||||
<Button type="default" @click="exportShow=false">{{$L('取消')}}</Button>
|
||||
<Button type="primary" :loading="exportLoadIng > 0" @click="onExport">{{$L('导出')}}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UserInput from "../../../../components/UserInput";
|
||||
export default {
|
||||
name: "SystemCheckin",
|
||||
components: {UserInput},
|
||||
data() {
|
||||
return {
|
||||
loadIng: 0,
|
||||
|
||||
formData: {
|
||||
wifi: '',
|
||||
cmd: '',
|
||||
},
|
||||
ruleData: {},
|
||||
|
||||
dateOptions: {
|
||||
shortcuts: [
|
||||
{
|
||||
text: this.$L('上个月'),
|
||||
value() {
|
||||
return [$A.getData('上个月', true), this.lastSecond($A.getData('上个月结束', true))];
|
||||
}
|
||||
},
|
||||
{
|
||||
text: this.$L('这个月'),
|
||||
value() {
|
||||
return [$A.getData('本周', true), this.lastSecond($A.getData('本月结束', true))];
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
exportShow: false,
|
||||
exportLoadIng: 0,
|
||||
exportData: {
|
||||
userid: [],
|
||||
date: [],
|
||||
time: [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.systemSetting();
|
||||
},
|
||||
|
||||
methods: {
|
||||
submitForm() {
|
||||
this.$refs.formData.validate((valid) => {
|
||||
if (valid) {
|
||||
this.systemSetting(true);
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
resetForm() {
|
||||
this.formData = $A.cloneJSON(this.formDatum_bak);
|
||||
},
|
||||
|
||||
systemSetting(save) {
|
||||
this.loadIng++;
|
||||
this.$store.dispatch("call", {
|
||||
url: 'system/setting/checkin?type=' + (save ? 'save' : 'all'),
|
||||
data: this.formData,
|
||||
}).then(({data}) => {
|
||||
if (save) {
|
||||
$A.messageSuccess('修改成功');
|
||||
}
|
||||
this.formData = data;
|
||||
this.formDatum_bak = $A.cloneJSON(this.formData);
|
||||
}).catch(({msg}) => {
|
||||
if (save) {
|
||||
$A.modalError(msg);
|
||||
}
|
||||
}).finally(_ => {
|
||||
this.loadIng--;
|
||||
});
|
||||
},
|
||||
|
||||
dateShortcuts(act) {
|
||||
const lastSecond = (e) => {
|
||||
return $A.Date($A.formatDate("Y-m-d 23:59:29", Math.round(e / 1000)))
|
||||
};
|
||||
if (act === 'prev') {
|
||||
return [$A.getData('上个月', true), lastSecond($A.getData('上个月结束', true))];
|
||||
} else if (act === 'this') {
|
||||
return [$A.getData('本月', true), lastSecond($A.getData('本月结束', true))]
|
||||
}
|
||||
},
|
||||
|
||||
clickCmd() {
|
||||
this.$nextTick(_ => {
|
||||
this.$refs.cmd.focus({cursor:'all'});
|
||||
});
|
||||
},
|
||||
|
||||
onExport() {
|
||||
if (this.exportLoadIng > 0) {
|
||||
return;
|
||||
}
|
||||
this.exportLoadIng++;
|
||||
this.$store.dispatch("call", {
|
||||
url: 'system/checkin/export',
|
||||
data: this.exportData,
|
||||
}).then(({data}) => {
|
||||
this.exportShow = false;
|
||||
this.$store.dispatch('downUrl', {
|
||||
url: data.url
|
||||
});
|
||||
}).catch(({msg}) => {
|
||||
$A.modalError(msg);
|
||||
}).finally(_ => {
|
||||
this.exportLoadIng--;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -71,6 +71,7 @@ export default {
|
||||
menu() {
|
||||
const menu = [
|
||||
{path: 'personal', name: '个人设置'},
|
||||
{path: 'checkin', name: '签到设置', desc: ' (Beta)'},
|
||||
{path: 'language', name: '语言设置'},
|
||||
{path: 'theme', name: '主题设置'},
|
||||
{path: 'password', name: '密码设置'},
|
||||
@ -114,7 +115,7 @@ export default {
|
||||
let name = '';
|
||||
menu.some((item) => {
|
||||
if (routeName === `manage-setting-${item.path}`) {
|
||||
name = item.name;
|
||||
name = `${item.name}${item.desc||''}`;
|
||||
return true;
|
||||
}
|
||||
})
|
||||
|
||||
@ -13,6 +13,9 @@
|
||||
<TabPane :label="$L('会议功能')" name="meeting">
|
||||
<SystemMeeting/>
|
||||
</TabPane>
|
||||
<TabPane :label="$L('签到功能')" name="checkin">
|
||||
<SystemCheckin/>
|
||||
</TabPane>
|
||||
<TabPane :label="$L('邮件设置')" name="emailSetting">
|
||||
<SystemEmailSetting/>
|
||||
</TabPane>
|
||||
@ -30,9 +33,11 @@ import SystemColumnTemplate from "./components/SystemColumnTemplate";
|
||||
import SystemEmailSetting from "./components/SystemEmailSetting";
|
||||
import SystemAppPush from "./components/SystemAppPush";
|
||||
import SystemMeeting from "./components/SystemMeeting";
|
||||
import SystemCheckin from "./components/SystemCheckin";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SystemCheckin,
|
||||
SystemMeeting,
|
||||
SystemAppPush, SystemColumnTemplate, SystemTaskPriority, SystemSetting, SystemEmailSetting},
|
||||
data() {
|
||||
|
||||
5
resources/assets/js/routes.js
vendored
5
resources/assets/js/routes.js
vendored
@ -39,6 +39,11 @@ export default [
|
||||
path: 'personal',
|
||||
component: () => import('./pages/manage/setting/personal.vue'),
|
||||
},
|
||||
{
|
||||
name: 'manage-setting-checkin',
|
||||
path: 'checkin',
|
||||
component: () => import('./pages/manage/setting/checkin.vue'),
|
||||
},
|
||||
{
|
||||
name: 'manage-setting-language',
|
||||
path: 'language',
|
||||
|
||||
21
resources/assets/sass/pages/page-setting.scss
vendored
21
resources/assets/sass/pages/page-setting.scss
vendored
@ -243,6 +243,13 @@
|
||||
.block-setting-placeholder {
|
||||
height: 8px;
|
||||
}
|
||||
.export-data {
|
||||
cursor: pointer;
|
||||
color: #2b85e4;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -297,6 +304,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
.page-setting-checkin-export-common {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
> em {
|
||||
cursor: pointer;
|
||||
color: #2b85e4;
|
||||
margin-left: 8px;
|
||||
font-style: normal;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-setting {
|
||||
.setting-head {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user