feat: 签到功能
This commit is contained in:
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user