养老补贴

master
lion 6 days ago
parent 0c9a9fa64c
commit 7c374ae7f9

@ -2,7 +2,7 @@ APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_URL=https://yxbd_fangke.ali251.langye.net
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
@ -11,8 +11,8 @@ LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_DATABASE=yxbd_fangke_ali2
DB_USERNAME=yxbd_fangke_ali2
DB_PASSWORD=
BROADCAST_DRIVER=log
@ -50,3 +50,5 @@ PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
SANCTUM_STATEFUL_DOMAINS=yxbd_fangke.ali251.langye.net,localhost,127.0.0.1,127.0.0.1:8020

1
.gitignore vendored

@ -13,3 +13,4 @@ npm-debug.log
yarn-error.log
/.idea
/.vscode
/database/backup/*.sql

@ -8,6 +8,7 @@ use App\Models\Department;
use App\Models\OperateLog;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Rap2hpoutre\FastExcel\FastExcel;
use Spatie\Permission\Models\Role;
@ -243,65 +244,221 @@ class AdminController extends CommonController
}
}
/**
* @OA\Post (
* path="/api/admin/import",
* tags={"后台管理"},
* summary="导入数据",
* description="",
* @OA\Parameter(name="file", in="query", @OA\Schema(type="object"), required=true, description="文件"),
* @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="token"),
* @OA\Response(
* response="200",
* description="导入用户"
* )
* )
*/
public function importPreview(Request $request)
{
try {
$rows = $this->parseImportRows($request);
$previewRows = [];
$existsCount = 0;
$missingDepartmentRows = [];
foreach ($rows as $row) {
if ($row['exists']) {
$existsCount++;
}
if (!$row['department_exists']) {
$missingDepartmentRows[] = $row['line_no'];
}
$previewRows[] = [
'line_no' => $row['line_no'],
'name' => $row['name'],
'username' => $row['username'],
'mobile' => $row['mobile'],
'department' => $row['department'],
'position' => $row['position'],
'birthday' => $row['birthday'],
'email' => $row['email'],
'password_filled' => $row['password_filled'],
'department_exists' => $row['department_exists'],
'exists' => $row['exists'],
'message' => $row['message'],
];
}
return $this->success([
'rows' => $previewRows,
'exists_count' => $existsCount,
'missing_department_rows' => $missingDepartmentRows,
'can_import' => count($missingDepartmentRows) === 0,
]);
} catch (\Exception $exception) {
return $this->fail([$exception->getCode(), $exception->getMessage()]);
}
}
public function importSubmit(Request $request)
{
try {
$rows = $this->parseImportRows($request);
$forceUpdate = (bool)$request->get('force_update', false);
$missingDepartmentNames = [];
$existsUsernames = [];
foreach ($rows as $row) {
if (!$row['department_exists']) {
$missingDepartmentNames[] = $row['department'];
}
if ($row['exists']) {
$existsUsernames[] = $row['username'];
}
}
if (!empty($missingDepartmentNames)) {
$missingDepartmentNames = array_values(array_unique($missingDepartmentNames));
return $this->fail([ResponseCode::ERROR_BUSINESS, '部门不存在:' . implode('、', $missingDepartmentNames) . ',请先创建部门后再导入']);
}
if (!$forceUpdate && !empty($existsUsernames)) {
$existsUsernames = array_values(array_unique($existsUsernames));
return $this->fail([ResponseCode::ERROR_BUSINESS, '用户' . implode('、', $existsUsernames) . '已存在,是否直接更新?']);
}
DB::beginTransaction();
$created = 0;
$updated = 0;
foreach ($rows as $row) {
$data = [
'name' => $row['name'],
'username' => $row['username'],
'department_id' => $row['department_id'],
'mobile' => $row['mobile'],
'position' => $row['position'] ?: null,
'birthday' => $row['birthday'],
'email' => $row['email'] ?: null,
];
$model = Admin::where('username', $row['username'])->first();
if ($model) {
if (!empty($row['password_plain'])) {
$data['password'] = Hash::make($row['password_plain']);
}
$model->update($data);
$updated++;
} else {
if (!empty($row['password_plain'])) {
$data['password'] = Hash::make($row['password_plain']);
} else {
$data['password'] = Hash::make('Admin' . date('Y'));
}
Admin::create($data);
$created++;
}
}
DB::commit();
return $this->success([
'created' => $created,
'updated' => $updated,
'total' => count($rows),
]);
} catch (\Exception $exception) {
if (DB::transactionLevel() > 0) {
DB::rollBack();
}
return $this->fail([$exception->getCode(), $exception->getMessage()]);
}
}
// 保留旧接口兼容,默认按“允许更新已存在用户”执行
public function import(Request $request)
{
$request->merge(['force_update' => 1]);
return $this->importSubmit($request);
}
private function parseImportRows(Request $request)
{
$file = $request->file('file');
//判断文件是否有效
if (!($request->hasFile('file') && $file->isValid())) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '文件不存在或无效']);
throw new \Exception('文件不存在或无效', ResponseCode::ERROR_BUSINESS);
}
//获取文件大小
$img_size = floor($file->getSize() / 1024);
if ($img_size >= 5 * 1024) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '文件必须小于5M']);
$sizeKb = floor($file->getSize() / 1024);
if ($sizeKb >= 5 * 1024) {
throw new \Exception('文件必须小于5M', ResponseCode::ERROR_BUSINESS);
}
//过滤文件后缀
$ext = $file->getClientOriginalExtension();
$ext = strtolower($file->getClientOriginalExtension());
if (!in_array($ext, ['xls', 'xlsx', 'csv'])) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '仅支持xls/xlsx/csv格式']);
throw new \Exception('仅支持xls/xlsx/csv格式', ResponseCode::ERROR_BUSINESS);
}
$dataArray = (new FastExcel)->import($file->getRealPath())->toArray();
if (empty($dataArray)) {
throw new \Exception('导入文件为空', ResponseCode::ERROR_BUSINESS);
}
$tempFile = $file->getRealPath();
$dataArray = (new FastExcel)->import($tempFile)->toArray();
// 获取所有key
$keyList = array_keys($dataArray[0]);
if (!in_array('GID登录用户名', $keyList)) {
return $this->fail([ResponseCode::ERROR_BUSINESS, 'GID登录用户名字段不存在']);
$requiredHeaders = ['姓名', '用户名', '手机号', '所属部门'];
foreach ($requiredHeaders as $header) {
if (!in_array($header, $keyList)) {
throw new \Exception($header . '字段不存在', ResponseCode::ERROR_BUSINESS);
}
}
$rows = [];
foreach ($dataArray as $index => $value) {
$name = trim((string)($value['姓名'] ?? ''));
$username = trim((string)($value['用户名'] ?? ''));
$mobile = trim((string)($value['手机号'] ?? ''));
$department = trim((string)($value['所属部门'] ?? ''));
$position = trim((string)($value['职位'] ?? ''));
$birthday = $this->normalizeImportBirthday($value['生日'] ?? null);
$email = trim((string)($value['邮箱'] ?? ''));
$passwordPlain = trim((string)($value['密码'] ?? ''));
$passwordFilled = $passwordPlain !== '';
if ($username === '' && $department === '' && $name === '' && $mobile === '') {
continue;
}
if ($username === '' || $department === '' || $name === '' || $mobile === '') {
throw new \Exception('第' . ($index + 2) . '行数据不完整(姓名/用户名/手机号/所属部门为必填)', ResponseCode::ERROR_BUSINESS);
}
$departmentId = Department::where('name', $department)->value('id');
$exists = Admin::where('username', $username)->exists();
$message = '';
if (!$departmentId) {
$message = '请先创建部门';
} elseif ($exists) {
$message = '用户' . $username . '已存在,是否直接更新';
}
$rows[] = [
'line_no' => $index + 2,
'username' => $username,
'department' => $department,
'department_id' => $departmentId,
'department_exists' => !empty($departmentId),
'name' => $name,
'mobile' => $mobile,
'position' => $position,
'birthday' => $birthday,
'email' => $email,
'password_plain' => $passwordPlain,
'password_filled' => $passwordFilled,
'exists' => $exists,
'message' => $message,
];
}
if (empty($rows)) {
throw new \Exception('导入文件无有效数据', ResponseCode::ERROR_BUSINESS);
}
if (!in_array('部门', $keyList)) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '部门字段不存在']);
return $rows;
}
/**
* 生日:支持 yyyy-MM-dd 文本或 Excel 日期序列号。
*/
private function normalizeImportBirthday($value): ?string
{
if ($value === null || $value === '') {
return null;
}
if (!in_array('姓名', $keyList)) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '姓名字段不存在']);
if (is_numeric($value)) {
$excel = (float)$value;
if ($excel > 20000 && $excel < 60000) {
$unix = (int)(($excel - 25569) * 86400);
return gmdate('Y-m-d', $unix);
}
}
if (!in_array('手机号码', $keyList)) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '手机号码字段不存在']);
$s = trim((string)$value);
if ($s === '') {
return null;
}
$list = [];
foreach ($dataArray as $key => $value) {
$departmentId = Department::where('name', $value['部门'])->value('id');
$whereArray = ['name' => $value['姓名']];
$updateDataArray = ['name' => $value['姓名'], 'username' => $value['GID登录用户名'],
'department_id' => $departmentId,
'mobile' => $value['手机号码'],
'password'=> \Illuminate\Support\Facades\Hash::make("Admin" . date("Y"))
];
Admin::updateOrCreate($whereArray, $updateDataArray);
if (preg_match('/^\d{4}-\d{2}-\d{2}/', $s)) {
return substr($s, 0, 10);
}
return $this->success($list);
return $s;
}
}

@ -10,6 +10,8 @@ use App\Models\Visit;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use App\Helpers\ResponseCode;
use Illuminate\Support\Carbon;
use Rap2hpoutre\FastExcel\FastExcel;
/**
@ -80,10 +82,46 @@ class GateController extends CommonController
if (isset($all['idcard'])) {
$query->where('idcard', $all['idcard']);
}
if (isset($all['start_date']) && isset($all['end_date'])) {
$query->whereBetween('date', [$all['start_date'], $all['end_date']]);
// 普通访客:按预约到访日 date 落在查询区间内;长期访客:查询区间与长期有效区间 [start_date,end_date] 有交集即可出现在「今日」等列表中
if (!empty($all['start_date']) && !empty($all['end_date'])) {
$qs = $all['start_date'];
$qe = $all['end_date'];
$query->where(function ($sub) use ($qs, $qe) {
$sub->where(function ($q1) use ($qs, $qe) {
$q1->where(function ($q2) {
$q2->whereNull('long_time')->orWhere('long_time', 0);
})->whereBetween('date', [$qs, $qe]);
})->orWhere(function ($q1) use ($qs, $qe) {
$q1->where('long_time', 1)
->whereNotNull('start_date')
->whereNotNull('end_date')
->where('start_date', '<=', $qe)
->where('end_date', '>=', $qs);
});
});
}
})->orderBy($all['sort_name'] ?? 'id', $all['sort_type'] ?? 'desc')->paginate($all['page_size'] ?? 20);;
})->orderBy($all['sort_name'] ?? 'id', $all['sort_type'] ?? 'desc');
if (isset($all['is_export']) && !empty($all['is_export'])) {
return (new FastExcel($list->limit(10000)->get()->toArray()))->download('门岗访客记录' . date('YmdHis') . '.csv', function ($info) {
return [
'编码' => $info['code'] ?? '',
'姓名' => $info['name'] ?? '',
'类型' => $info['type_text'] ?? '',
'审核状态' => $info['audit_status_text'] ?? '',
'被访人' => ($info['accept_admin']['name']) ?? '',
'预约日期' => $info['date'] ?? '',
'证件号' => $info['idcard'] ?? '',
'手机号' => $info['mobile'] ?? '',
'单位名称' => $info['company_name'] ?? '',
'到访时段开始' => ($info['visit_time']['start_time']) ?? '',
'到访时段结束' => ($info['visit_time']['end_time']) ?? '',
'创建时间' => $info['created_at'] ?? '',
];
});
}
$list = $list->paginate($all['page_size'] ?? 20);
return $this->success($list);
}
@ -124,11 +162,52 @@ class GateController extends CommonController
if (empty($check)) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '拜访记录不存在']);
}
if ($check->audit_status == 2 || $check->audit_status == 5) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '当前记录不可核销']);
}
// 物流访客:每次进/离厂都必须先上传当次车辆照片
if ((int)$check->type === 3) {
$latestGateLog = GateLog::where('visit_id', $check->id)->orderByDesc('id')->first();
$fileIds = [];
if (is_array($check->file)) {
$fileIds = $check->file;
} elseif (is_string($check->file) && !empty($check->file)) {
$decoded = json_decode($check->file, true);
if (is_array($decoded)) {
$fileIds = $decoded;
}
}
$hasPhoto = count($fileIds) > 0;
if (!$hasPhoto) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '物流访客需先上传车辆照片后再核销']);
}
// 要求本次核销前有一次新的更新动作,避免重复使用上次照片直接核销
if (!empty($latestGateLog) && strtotime((string)$check->updated_at) <= strtotime((string)$latestGateLog->created_at)) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '物流访客本次进/离厂请先上传当次照片']);
}
}
// 长期访客:校验当前日期在有效周期内
$today = Carbon::now()->format('Y-m-d');
if ((int)$check->long_time === 1) {
if (empty($check->start_date) || empty($check->end_date)) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '长期访客未配置有效周期']);
}
if ($today < $check->start_date || $today > $check->end_date) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '当前不在长期访客有效周期内']);
}
$todayLastLog = GateLog::where('visit_id', $check->id)->where('biz_date', $today)->orderByDesc('id')->first();
if ((int)$all['type'] === 1 && $todayLastLog && (int)$todayLastLog->action === 1) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '今日已进厂,请先离厂后再进厂']);
}
if ((int)$all['type'] === 2 && (!$todayLastLog || (int)$todayLastLog->action !== 1)) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '今日无有效进厂记录,无法离厂']);
}
}
$remark = '进厂';
if ($all['type'] == 2) {
$remark = '离厂';
}
$gateLog = GateLog::add($all['admin_id'], $all['code'], $all['person_no'] ?? [], $all['car_no'] ?? [], $remark);
$gateLog = GateLog::add($all['admin_id'], $all['code'], $all['person_no'] ?? [], $all['car_no'] ?? [], $remark, $check->id, (int)$all['type'], $today);
if ($all['type'] == 1) {
// 入场
Visit::where('code', $all['code'])->update(['audit_status' => 3, 'person_no' => $all['person_no'] ?? '', 'car_no' => $all['car_no'] ?? '']);

@ -0,0 +1,164 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Helpers\ResponseCode;
use App\Models\VipCustomer;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Rap2hpoutre\FastExcel\FastExcel;
use Illuminate\Http\Request;
class VipCustomerController extends CommonController
{
public function index()
{
$all = request()->all();
$list = VipCustomer::where(function ($query) use ($all) {
if (!empty($all['keyword'])) {
$keyword = trim($all['keyword']);
$query->where(function ($q) use ($keyword) {
$q->where('name', 'like', '%' . $keyword . '%')
->orWhere('mobile', 'like', '%' . $keyword . '%')
->orWhere('company_name', 'like', '%' . $keyword . '%')
->orWhere('idcard', 'like', '%' . $keyword . '%')
->orWhere('plate_no', 'like', '%' . $keyword . '%');
});
}
if (isset($all['status']) && $all['status'] !== '') {
$query->where('status', $all['status']);
}
if (isset($all['credent']) && $all['credent'] !== '') {
$query->where('credent', $all['credent']);
}
})->orderBy($all['sort_name'] ?? 'id', $all['sort_type'] ?? 'desc');
if (isset($all['is_export']) && !empty($all['is_export'])) {
return (new FastExcel($list->limit(5000)->get()->toArray()))->download('VIP客户列表' . date('YmdHis') . '.xlsx', function ($info) {
return [
'姓名' => $info['name'] ?? '',
'手机号' => $info['mobile'] ?? '',
'证件类型' => ($info['credent'] ?? 1) == 1 ? '身份证' : '护照',
'证件号码' => $info['idcard'] ?? '',
'车牌号' => $info['plate_no'] ?? '',
'单位名称' => $info['company_name'] ?? '',
'职位' => $info['position'] ?? '',
'状态' => ($info['status'] ?? 1) == 1 ? '启用' : '禁用',
'备注' => $info['remark'] ?? '',
'创建时间' => $info['created_at'] ?? '',
];
});
}
$list = $list->paginate($all['page_size'] ?? 20);
return $this->success($list);
}
public function show()
{
$all = request()->all();
$validator = Validator::make($all, [
'id' => 'required',
], [
'id.required' => 'Id必填',
]);
if ($validator->fails()) {
return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
$detail = VipCustomer::find($all['id']);
return $this->success($detail);
}
public function save()
{
$all = request()->all();
$validator = Validator::make($all, [
'name' => 'required',
'mobile' => 'required',
], [
'name.required' => '姓名必填',
'mobile.required' => '手机号必填',
]);
if ($validator->fails()) {
return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
DB::beginTransaction();
try {
if (isset($all['id']) && $all['id']) {
$model = VipCustomer::find($all['id']);
} else {
$model = new VipCustomer();
$all['admin_id'] = $this->getUserId();
$all['department_id'] = $this->getUser()->department_id;
}
$model->fill($all);
$model->save();
DB::commit();
return $this->success('更新成功');
} catch (\Exception $exception) {
DB::rollBack();
return $this->fail([$exception->getCode(), $exception->getMessage()]);
}
}
public function destroy()
{
$all = request()->all();
$validator = Validator::make($all, [
'id' => 'required',
], [
'id.required' => 'Id必填',
]);
if ($validator->fails()) {
return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
VipCustomer::where('id', $all['id'])->delete();
return $this->success('删除成功');
}
public function import(Request $request)
{
$file = $request->file('file');
if (!($request->hasFile('file') && $file->isValid())) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '文件不存在或无效']);
}
$ext = $file->getClientOriginalExtension();
if (!in_array($ext, ['xlsx'])) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '仅支持xlsx格式']);
}
$tempFile = $file->getRealPath();
$dataArray = (new FastExcel)->import($tempFile)->toArray();
if (empty($dataArray)) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '导入文件为空']);
}
$keyList = array_keys($dataArray[0]);
if (!in_array('姓名', $keyList) || !in_array('手机号', $keyList)) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '模板字段缺失,至少包含:姓名、手机号']);
}
foreach ($dataArray as $value) {
$name = trim($value['姓名'] ?? '');
$mobile = trim($value['手机号'] ?? '');
if (empty($name) || empty($mobile)) {
continue;
}
$credentText = trim($value['证件类型'] ?? '身份证');
$credent = $credentText === '护照' ? 2 : 1;
VipCustomer::updateOrCreate(
['mobile' => $mobile],
[
'name' => $name,
'credent' => $credent,
'idcard' => trim($value['证件号码'] ?? ''),
'plate_no' => trim($value['车牌号'] ?? ''),
'company_name' => trim($value['单位名称'] ?? ''),
'position' => trim($value['职位'] ?? ''),
'remark' => trim($value['备注'] ?? ''),
'status' => trim($value['状态'] ?? '') === '禁用' ? 2 : 1,
'admin_id' => $this->getUserId(),
'department_id' => $this->getUser()->department_id,
]
);
}
return $this->success('导入成功');
}
}

@ -35,6 +35,7 @@ class VisitController extends CommonController
* @OA\Parameter(name="my_audit", in="query", @OA\Schema(type="string"), required=false, description="是否显示我审核的记录0否1是默认0"),
* @OA\Parameter(name="my_accept_admin", in="query", @OA\Schema(type="string"), required=false, description="是否显示接待人员是自己的0否1是默认0"),
* @OA\Parameter(name="long_time", in="query", @OA\Schema(type="string"), required=false, description="是否长期访客0否1是"),
* @OA\Parameter(name="type", in="query", @OA\Schema(type="string"), required=false, description="访客类型1普通2施工3物流4VIP"),
* @OA\Parameter(name="is_auth", in="query", @OA\Schema(type="string"), required=true, description="is_auth是否鉴权0否1是"),
* @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="token"),
* @OA\Response(
@ -54,8 +55,23 @@ class VisitController extends CommonController
if (isset($all['audit_status'])) {
$query->where('audit_status', $all['audit_status']);
}
if (isset($all['start_date']) && isset($all['end_date'])) {
$query->whereBetween('date', [$all['start_date'], $all['end_date']]);
// 起始时间:普通访客按 date 落在查询区间内;长期访客按查询区间与长期 [start_date,end_date] 有交集(与门岗列表一致)
if (!empty($all['start_date']) && !empty($all['end_date'])) {
$qs = $all['start_date'];
$qe = $all['end_date'];
$query->where(function ($sub) use ($qs, $qe) {
$sub->where(function ($q1) use ($qs, $qe) {
$q1->where(function ($q2) {
$q2->whereNull('long_time')->orWhere('long_time', 0);
})->whereBetween('date', [$qs, $qe]);
})->orWhere(function ($q1) use ($qs, $qe) {
$q1->where('long_time', 1)
->whereNotNull('start_date')
->whereNotNull('end_date')
->where('start_date', '<=', $qe)
->where('end_date', '>=', $qs);
});
});
}
if (isset($all['my_self']) && !empty($all['my_self'])) {
$query->where('admin_id', $this->getUserId());
@ -66,6 +82,9 @@ class VisitController extends CommonController
if (isset($all['long_time']) && !empty($all['long_time'])) {
$query->where('long_time', $all['long_time']);
}
if (isset($all['type']) && $all['type'] !== '' && $all['type'] !== null) {
$query->where('type', $all['type']);
}
if (isset($all['my_audit']) && !empty($all['my_audit'])) {
$query->whereHas('audit', function ($q) {
$q->where('audit_admin_id', $this->getUserId());
@ -132,7 +151,27 @@ class VisitController extends CommonController
if ($validator->fails()) {
return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
$detail = Visit::with('accompany.department', 'logs.admin', 'logs.user', 'audit.auditAdmin', 'visitTime', 'acceptAdmin.department', 'acceptAdminSignFile', 'acceptGoodsAdmin.department', 'visitArea', 'audit.auditAdmin')->find($all['id']);
$detail = Visit::with('accompany.department', 'logs.admin', 'logs.user', 'gateLogs.admin', 'audit.auditAdmin', 'visitTime', 'acceptAdmin.department', 'acceptAdminSignFile', 'acceptGoodsAdmin.department', 'visitArea', 'audit.auditAdmin')->find($all['id']);
if (!empty($detail)) {
$dailyGateRecords = collect($detail->gateLogs ?? [])->groupBy('biz_date')->map(function ($records, $date) {
return [
'biz_date' => $date,
'records' => collect($records)->map(function ($item) {
return [
'id' => $item->id,
'action' => $item->action,
'action_text' => $item->action_text,
'remark' => $item->remark,
'created_at' => $item->created_at,
'admin_name' => $item->admin->name ?? '',
'person_no' => $item->person_no ?? [],
'car_no' => $item->car_no ?? [],
];
})->values(),
];
})->values();
$detail->setAttribute('daily_gate_records', $dailyGateRecords);
}
return $this->success($detail);
}

@ -6,6 +6,7 @@ use App\Helpers\ResponseCode;
use App\Helpers\StarterResponseCode;
use App\Models\Config;
use App\Models\User;
use App\Models\VipCustomer;
use App\Models\Visit;
use EasyWeChat\Factory;
use Illuminate\Support\Facades\Validator;
@ -138,7 +139,30 @@ class UserController extends CommonController
*/
public function show()
{
return $this->success($this->guard()->user());
$user = $this->guard()->user();
$isVip = false;
if (!empty($user->mobile)) {
$isVip = VipCustomer::where('mobile', $user->mobile)->where('status', 1)->exists();
}
return $this->success([
...$user->toArray(),
'is_vip' => $isVip ? 1 : 0,
]);
}
public function isVip()
{
$all = request()->all();
$validator = Validator::make($all, [
'mobile' => 'required',
], [
'mobile.required' => '手机号必填',
]);
if ($validator->fails()) {
return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
$isVip = VipCustomer::where('mobile', $all['mobile'])->where('status', 1)->exists();
return $this->success(['is_vip' => $isVip ? 1 : 0]);
}
/**

@ -7,6 +7,8 @@ use App\Models\Admin;
use App\Models\Department;
use App\Models\Study;
use App\Models\StudyLog;
use App\Models\User;
use App\Models\VipCustomer;
use App\Models\Visit;
use App\Models\VisitArea;
use App\Models\VisitAudit;
@ -20,6 +22,46 @@ use Illuminate\Support\Facades\Validator;
class VisitController extends CommonController
{
/**
* 获取当前用户最近一条拜访人信息(按手机号/证件号匹配)
*/
public function latestVisitor()
{
$all = request()->all();
$mobile = trim((string)($all['mobile'] ?? ''));
$idcard = trim((string)($all['idcard'] ?? ''));
if ($mobile === '' && $idcard === '') {
return $this->fail([ResponseCode::ERROR_PARAMETER, '手机号或证件号至少填写一个']);
}
$latest = Visit::where('user_id', $this->getUserId())
->where(function ($q) use ($mobile, $idcard) {
if ($mobile !== '') {
$q->orWhere('mobile', $mobile);
}
if ($idcard !== '') {
$q->orWhere('idcard', $idcard);
}
})
->orderByDesc('id')
->first();
if (empty($latest)) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '未找到过往拜访记录']);
}
return $this->success([
'name' => $latest->name ?? '',
'mobile' => $latest->mobile ?? '',
'credent' => $latest->credent ?? 1,
'idcard' => $latest->idcard ?? '',
'company_name' => $latest->company_name ?? '',
'cda' => $latest->cda ?? '',
'cars' => $latest->cars ?? [],
]);
}
/**
* @OA\Post(
* path="/api/mobile/visit/visit-save",
@ -190,7 +232,22 @@ class VisitController extends CommonController
return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
$detail = Study::with('asks')->where('type', $all['type'])->first();
return $this->success($detail);
if (empty($detail)) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '该访客类型暂无学习资料,请联系管理员配置']);
}
$mobile = trim((string)($all['mobile'] ?? ''));
if ($mobile === '') {
$mobile = trim((string)(optional(User::find($this->getUserId()))->mobile ?? ''));
}
$isVip = $mobile !== '' && VipCustomer::where('mobile', $mobile)->where('status', 1)->exists();
$payload = $detail->toArray();
$payload['is_vip'] = $isVip ? 1 : 0;
// VIP仅观看资料不参与问答非 VIP必须答题
$payload['quiz_required'] = $isVip ? 0 : 1;
return $this->success($payload);
}
/**
@ -257,13 +314,14 @@ class VisitController extends CommonController
public function askLog()
{
$type = request('type');
// 按「访客类型」分别记录type=1/2/3 各自一条有效学习过期后需重新学习并答题VIP 为仅观看记录)
$log = StudyLog::where('type', $type)->where('user_id', $this->getUserId())->orderBy('id', 'desc')->first();
if (empty($log)) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '未学习']);
}
$diff = Carbon::parse($log->created_at)->diffInDays(Carbon::now());
if ($diff > $log->expire_day) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '学习过期']);
return $this->fail([ResponseCode::ERROR_BUSINESS, '学习过期,请重新学习']);
}
return $this->success("学习有效中");
}
@ -291,10 +349,53 @@ class VisitController extends CommonController
public function askSave()
{
$all = request()->all();
$type = $all['type'] ?? null;
if ($type === null || $type === '') {
return $this->fail([ResponseCode::ERROR_PARAMETER, '类型必填']);
}
$study = Study::where('type', $type)->first();
if (empty($study)) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '该访客类型暂无学习资料配置']);
}
$mobile = trim((string)($all['mobile'] ?? ''));
if ($mobile === '') {
$mobile = trim((string)(optional(User::find($this->getUserId()))->mobile ?? ''));
}
$isVip = $mobile !== '' && VipCustomer::where('mobile', $mobile)->where('status', 1)->exists();
// 有效期以资料配置为准(天),不信任客户端篡改
$expireDay = $study->expire_day;
if ($expireDay === null || $expireDay === '') {
$expireDay = $all['expire_day'] ?? 180;
}
$all['expire_day'] = (int) $expireDay;
if (isset($all['ask']) && is_string($all['ask'])) {
$all['ask'] = json_decode($all['ask'], true);
}
if (isset($all['content']) && is_string($all['content'])) {
$all['content'] = json_decode($all['content'], true);
}
if ($isVip) {
$all['watch_only'] = 1;
$all['ask'] = [];
$all['content'] = $all['content'] ?? [];
} else {
$all['watch_only'] = 0;
$ask = $all['ask'] ?? null;
if (empty($ask) || !is_array($ask) || count($ask) === 0) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '请完成答题后再提交']);
}
}
$model = new StudyLog();
$all['user_id'] = $this->getUserId();
$model->fill($all);
$res = $model->save();
$model->save();
return $this->success($model);
}
@ -302,13 +403,14 @@ class VisitController extends CommonController
* @OA\Post(
* path="/api/mobile/visit/idcard-check",
* tags={"小程序-学习"},
* summary="保存学习记录",
* summary="按类型校验证件学习有效性",
* description="",
* @OA\Parameter(name="idcard", in="query", @OA\Schema(type="string"), required=true, description="身份证数组"),
* @OA\Parameter(name="type", in="query", @OA\Schema(type="string"), required=true, description="访客类型1普通2施工3物流"),
* @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="token"),
* @OA\Response(
* response="200",
* description="暂无"
* description="返回 missing(未学习), expired(已过期), invalid(合并) 三个数组"
* )
* )
*/
@ -316,21 +418,46 @@ class VisitController extends CommonController
{
$all = request()->all();
$messages = [
'idcard.required' => '身份证数组必填'
'idcard.required' => '身份证数组必填',
'type.required' => '访客类型必填',
];
$validator = Validator::make($all, [
'idcard' => 'required'
'idcard' => 'required',
'type' => 'required',
], $messages);
if ($validator->fails()) {
return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
$model = new StudyLog();
$list = $model->whereIn('idcard', $all['idcard'])->pluck('idcard');
$diff = collect($all['idcard'])->diff($list);
if ($diff->isNotEmpty()) {
$diff = array_values($diff->toArray());
$type = $all['type'];
$missingIdcards = [];
$expiredIdcards = [];
foreach ((array)$all['idcard'] as $idcard) {
$idcard = trim((string)$idcard);
if ($idcard === '') {
continue;
}
$log = StudyLog::where('idcard', $idcard)
->where('type', $type)
->orderBy('id', 'desc')
->first();
if (empty($log)) {
$missingIdcards[] = $idcard;
continue;
}
$diff = Carbon::parse($log->created_at)->diffInDays(Carbon::now());
if ($diff > (int)$log->expire_day) {
$expiredIdcards[] = $idcard;
}
}
return $this->success($diff);
$missingIdcards = array_values(array_unique($missingIdcards));
$expiredIdcards = array_values(array_unique($expiredIdcards));
return $this->success([
'missing' => $missingIdcards,
'expired' => $expiredIdcards,
// 兼容旧前端判断
'invalid' => array_values(array_unique(array_merge($missingIdcards, $expiredIdcards))),
]);
}
}

@ -11,14 +11,30 @@ class GateLog extends CommonModel
'car_no' => 'json'
];
public static function add($admin_id, $code, $person_no, $car_no, $remark = '')
protected $appends = ['action_text'];
public function getActionTextAttribute()
{
$array = [1 => '进厂', 2 => '离厂'];
return $array[$this->action] ?? ($this->remark ?? '');
}
public function admin()
{
return $this->hasOne(Admin::class, 'id', 'admin_id');
}
public static function add($admin_id, $code, $person_no, $car_no, $remark = '', $visit_id = null, $action = null, $biz_date = null)
{
return self::create([
'admin_id' => $admin_id,
'visit_id' => $visit_id,
'code' => $code,
'action' => $action,
'car_no' => $car_no,
'person_no' => $person_no,
'remark' => $remark
'remark' => $remark,
'biz_date' => $biz_date
]);
}

@ -9,6 +9,7 @@ class StudyLog extends CommonModel
protected $casts = [
'content'=>'json',
'ask'=>'json',
'watch_only' => 'boolean',
];
public function user(){

@ -0,0 +1,9 @@
<?php
namespace App\Models;
class VipCustomer extends SoftDeletesModel
{
protected $guarded = ['id'];
}

@ -28,7 +28,7 @@ class Visit extends SoftDeletesModel
public function getTypeTextAttribute()
{
$array = [1 => '普通访客', 2 => '施工访客', 3 => '物流访客'];
$array = [1 => '普通访客', 2 => '施工访客', 3 => '物流访客', 4 => 'VIP访客'];
return $array[$this->type] ?? '';
}

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('vip_customers', function (Blueprint $table) {
$table->increments('id');
$table->integer('admin_id')->nullable()->comment('创建人');
$table->integer('department_id')->nullable()->comment('创建人部门');
$table->string('name')->comment('姓名');
$table->string('mobile', 20)->comment('手机号')->index();
$table->string('company_name')->nullable()->comment('单位名称');
$table->string('position')->nullable()->comment('职位');
$table->tinyInteger('status')->default(1)->comment('状态 1启用 2禁用');
$table->string('remark')->nullable()->comment('备注');
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('vip_customers');
}
};

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('vip_customers', function (Blueprint $table) {
$table->tinyInteger('credent')->default(1)->comment('证件类型 1身份证 2护照')->after('mobile');
$table->string('idcard')->nullable()->comment('证件号码')->after('credent');
$table->string('plate_no')->nullable()->comment('车牌号')->after('idcard');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('vip_customers', function (Blueprint $table) {
$table->dropColumn(['credent', 'idcard', 'plate_no']);
});
}
};

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('gate_logs', function (Blueprint $table) {
$table->integer('visit_id')->nullable()->comment('拜访记录id')->after('admin_id');
$table->tinyInteger('action')->nullable()->comment('动作 1进厂 2离厂')->after('code');
$table->date('biz_date')->nullable()->comment('业务日期')->after('remark');
$table->index(['visit_id', 'biz_date'], 'idx_gate_logs_visit_biz_date');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('gate_logs', function (Blueprint $table) {
$table->dropIndex('idx_gate_logs_visit_biz_date');
$table->dropColumn(['visit_id', 'action', 'biz_date']);
});
}
};

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::table('study_logs', function (Blueprint $table) {
$table->boolean('watch_only')->default(0)->after('type')->comment('1仅观看不答题(VIP)');
});
}
public function down()
{
Schema::table('study_logs', function (Blueprint $table) {
$table->dropColumn('watch_only');
});
}
};

@ -82,6 +82,17 @@ Route::group(["namespace" => "Admin", "prefix" => "admin", "middleware" => "sanc
Route::get("blacklist/show", [\App\Http\Controllers\Admin\BlacklistController::class, "show"]);
Route::post("blacklist/save", [\App\Http\Controllers\Admin\BlacklistController::class, "save"]);
Route::get("blacklist/destroy", [\App\Http\Controllers\Admin\BlacklistController::class, "destroy"]);
// VIP客户管理
Route::get("vip-customer/index", [\App\Http\Controllers\Admin\VipCustomerController::class, "index"]);
Route::get("vip-customer/show", [\App\Http\Controllers\Admin\VipCustomerController::class, "show"]);
Route::post("vip-customer/save", [\App\Http\Controllers\Admin\VipCustomerController::class, "save"]);
Route::get("vip-customer/destroy", [\App\Http\Controllers\Admin\VipCustomerController::class, "destroy"]);
Route::post("vip-customer/import", [\App\Http\Controllers\Admin\VipCustomerController::class, "import"]);
// 用户导入
Route::post("admin/import-preview", [\App\Http\Controllers\Admin\AdminController::class, "importPreview"]);
Route::post("admin/import-submit", [\App\Http\Controllers\Admin\AdminController::class, "importSubmit"]);
});
// 前台
@ -91,10 +102,12 @@ Route::group(["namespace" => "Mobile", "prefix" => "mobile", "middleware" => "sa
Route::post('user/save', [\App\Http\Controllers\Mobile\UserController::class, 'save']);
Route::get('user/mobile', [\App\Http\Controllers\Mobile\UserController::class, 'mobile']);
Route::get('user/show', [\App\Http\Controllers\Mobile\UserController::class, 'show']);
Route::get('user/is-vip', [\App\Http\Controllers\Mobile\UserController::class, 'isVip']);
Route::get('user/my-visit', [\App\Http\Controllers\Mobile\UserController::class, 'myVisit']);
Route::get('user/my-visit-detail', [\App\Http\Controllers\Mobile\UserController::class, 'myVisitDetail']);
Route::post('visit/visit-save', [\App\Http\Controllers\Mobile\VisitController::class, 'visitSave']);
Route::get('visit/latest-visitor', [\App\Http\Controllers\Mobile\VisitController::class, 'latestVisitor']);
Route::get('visit/get-ask', [\App\Http\Controllers\Mobile\VisitController::class, 'getAsk']);
Route::get('visit/visit-area', [\App\Http\Controllers\Mobile\VisitController::class, 'visitArea']);
Route::get('visit/visit-time', [\App\Http\Controllers\Mobile\VisitController::class, 'visitTime']);

@ -0,0 +1,13 @@
#!/usr/bin/env bash
# 将本地 bd_fangke_ali251 导出为 SQL 文件,便于上传到服务器后导入 yxbd_fangke_ali2。
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
mkdir -p database/backup
OUT="${MYSQL_DUMP_FILE:-$ROOT/database/backup/bd_fangke_ali251_$(date +%Y%m%d_%H%M%S).sql}"
mysqldump -h"${MYSQL_SOURCE_HOST:-127.0.0.1}" -P"${MYSQL_SOURCE_PORT:-3306}" \
-u"${MYSQL_SOURCE_USER:-root}" -p"${MYSQL_SOURCE_PASSWORD:-root123456}" \
--single-transaction --routines --triggers --default-character-set=utf8mb4 \
"${MYSQL_SOURCE_DB:-bd_fangke_ali251}" > "$OUT"
echo "Wrote: $OUT"
ls -lh "$OUT"

@ -0,0 +1,37 @@
#!/usr/bin/env bash
# 将本地/源库 bd_fangke_ali251 的结构与数据导入目标库 yxbd_fangke_ali2。
# 用法(在能访问目标 MySQL 的机器上执行):
# export MYSQL_TARGET_HOST=127.0.0.1 # 或 RDS 内网地址
# export MYSQL_TARGET_PORT=3306
# export MYSQL_TARGET_USER=yxbd_fangke_ali2
# export MYSQL_TARGET_PASSWORD='jsy6WQHMR67kY7dN'
# export MYSQL_TARGET_DB=yxbd_fangke_ali2
# # 源库(默认本机 root与开发 .env 一致)
# export MYSQL_SOURCE_HOST=127.0.0.1
# export MYSQL_SOURCE_USER=root
# export MYSQL_SOURCE_PASSWORD=root123456
# export MYSQL_SOURCE_DB=bd_fangke_ali251
# bash scripts/migrate-db-to-yxbd-fangke-ali2.sh
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
MYSQL_SOURCE_HOST="${MYSQL_SOURCE_HOST:-127.0.0.1}"
MYSQL_SOURCE_PORT="${MYSQL_SOURCE_PORT:-3306}"
MYSQL_TARGET_PORT="${MYSQL_TARGET_PORT:-3306}"
MYSQL_SOURCE_USER="${MYSQL_SOURCE_USER:-root}"
MYSQL_SOURCE_PASSWORD="${MYSQL_SOURCE_PASSWORD:-root123456}"
MYSQL_SOURCE_DB="${MYSQL_SOURCE_DB:-bd_fangke_ali251}"
MYSQL_TARGET_HOST="${MYSQL_TARGET_HOST:-127.0.0.1}"
MYSQL_TARGET_USER="${MYSQL_TARGET_USER:-yxbd_fangke_ali2}"
MYSQL_TARGET_PASSWORD="${MYSQL_TARGET_PASSWORD:-jsy6WQHMR67kY7dN}"
MYSQL_TARGET_DB="${MYSQL_TARGET_DB:-yxbd_fangke_ali2}"
echo "Dumping ${MYSQL_SOURCE_DB} from ${MYSQL_SOURCE_HOST} ..."
mysqldump -h"$MYSQL_SOURCE_HOST" -P"$MYSQL_SOURCE_PORT" -u"$MYSQL_SOURCE_USER" -p"$MYSQL_SOURCE_PASSWORD" \
--single-transaction --routines --triggers --default-character-set=utf8mb4 \
"$MYSQL_SOURCE_DB" | \
mysql -h"$MYSQL_TARGET_HOST" -P"$MYSQL_TARGET_PORT" -u"$MYSQL_TARGET_USER" -p"$MYSQL_TARGET_PASSWORD" "$MYSQL_TARGET_DB"
echo "Import finished into ${MYSQL_TARGET_DB}@${MYSQL_TARGET_HOST}."
Loading…
Cancel
Save