From 7c374ae7f9d7ee37ca1c3bf3f29b7b8898b73670 Mon Sep 17 00:00:00 2001 From: lion <120344285@qq.com> Date: Mon, 30 Mar 2026 13:48:39 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=BB=E8=80=81=E8=A1=A5=E8=B4=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 8 +- .gitignore | 1 + .../Controllers/Admin/AdminController.php | 247 ++++++++++++++---- app/Http/Controllers/Admin/GateController.php | 87 +++++- .../Admin/VipCustomerController.php | 164 ++++++++++++ .../Controllers/Admin/VisitController.php | 45 +++- .../Controllers/Mobile/UserController.php | 26 +- .../Controllers/Mobile/VisitController.php | 153 ++++++++++- app/Models/GateLog.php | 20 +- app/Models/StudyLog.php | 1 + app/Models/VipCustomer.php | 9 + app/Models/Visit.php | 2 +- ...3_23_160000_create_vip_customers_table.php | 37 +++ ...03_23_163000_alter_vip_customers_table.php | 31 +++ ...70000_alter_gate_logs_add_visit_fields.php | 33 +++ ...000_add_watch_only_to_study_logs_table.php | 22 ++ routes/api.php | 13 + scripts/dump-bd-fangke-ali251-to-file.sh | 13 + scripts/migrate-db-to-yxbd-fangke-ali2.sh | 37 +++ 19 files changed, 877 insertions(+), 72 deletions(-) create mode 100644 app/Http/Controllers/Admin/VipCustomerController.php create mode 100644 app/Models/VipCustomer.php create mode 100644 database/migrations/2026_03_23_160000_create_vip_customers_table.php create mode 100644 database/migrations/2026_03_23_163000_alter_vip_customers_table.php create mode 100644 database/migrations/2026_03_23_170000_alter_gate_logs_add_visit_fields.php create mode 100644 database/migrations/2026_03_25_120000_add_watch_only_to_study_logs_table.php create mode 100755 scripts/dump-bd-fangke-ali251-to-file.sh create mode 100755 scripts/migrate-db-to-yxbd-fangke-ali2.sh diff --git a/.env.example b/.env.example index 9bb1bd7..d1ef84f 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore index 4cd4b9a..4586ff3 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ npm-debug.log yarn-error.log /.idea /.vscode +/database/backup/*.sql diff --git a/app/Http/Controllers/Admin/AdminController.php b/app/Http/Controllers/Admin/AdminController.php index 0b5c85d..ba6840e 100644 --- a/app/Http/Controllers/Admin/AdminController.php +++ b/app/Http/Controllers/Admin/AdminController.php @@ -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; } } diff --git a/app/Http/Controllers/Admin/GateController.php b/app/Http/Controllers/Admin/GateController.php index 85ca655..bb0deb1 100644 --- a/app/Http/Controllers/Admin/GateController.php +++ b/app/Http/Controllers/Admin/GateController.php @@ -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'] ?? '']); diff --git a/app/Http/Controllers/Admin/VipCustomerController.php b/app/Http/Controllers/Admin/VipCustomerController.php new file mode 100644 index 0000000..f2a9da9 --- /dev/null +++ b/app/Http/Controllers/Admin/VipCustomerController.php @@ -0,0 +1,164 @@ +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('导入成功'); + } +} + diff --git a/app/Http/Controllers/Admin/VisitController.php b/app/Http/Controllers/Admin/VisitController.php index e60fd55..d2808e6 100644 --- a/app/Http/Controllers/Admin/VisitController.php +++ b/app/Http/Controllers/Admin/VisitController.php @@ -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); } diff --git a/app/Http/Controllers/Mobile/UserController.php b/app/Http/Controllers/Mobile/UserController.php index 3afcff6..2153026 100644 --- a/app/Http/Controllers/Mobile/UserController.php +++ b/app/Http/Controllers/Mobile/UserController.php @@ -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]); } /** diff --git a/app/Http/Controllers/Mobile/VisitController.php b/app/Http/Controllers/Mobile/VisitController.php index dd4c559..9f99bf4 100644 --- a/app/Http/Controllers/Mobile/VisitController.php +++ b/app/Http/Controllers/Mobile/VisitController.php @@ -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))), + ]); } } diff --git a/app/Models/GateLog.php b/app/Models/GateLog.php index 789d02d..0b68541 100644 --- a/app/Models/GateLog.php +++ b/app/Models/GateLog.php @@ -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 ]); } diff --git a/app/Models/StudyLog.php b/app/Models/StudyLog.php index f468334..74681e3 100644 --- a/app/Models/StudyLog.php +++ b/app/Models/StudyLog.php @@ -9,6 +9,7 @@ class StudyLog extends CommonModel protected $casts = [ 'content'=>'json', 'ask'=>'json', + 'watch_only' => 'boolean', ]; public function user(){ diff --git a/app/Models/VipCustomer.php b/app/Models/VipCustomer.php new file mode 100644 index 0000000..9a74af7 --- /dev/null +++ b/app/Models/VipCustomer.php @@ -0,0 +1,9 @@ + '普通访客', 2 => '施工访客', 3 => '物流访客']; + $array = [1 => '普通访客', 2 => '施工访客', 3 => '物流访客', 4 => 'VIP访客']; return $array[$this->type] ?? ''; } diff --git a/database/migrations/2026_03_23_160000_create_vip_customers_table.php b/database/migrations/2026_03_23_160000_create_vip_customers_table.php new file mode 100644 index 0000000..e753a53 --- /dev/null +++ b/database/migrations/2026_03_23_160000_create_vip_customers_table.php @@ -0,0 +1,37 @@ +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'); + } +}; + diff --git a/database/migrations/2026_03_23_163000_alter_vip_customers_table.php b/database/migrations/2026_03_23_163000_alter_vip_customers_table.php new file mode 100644 index 0000000..0445681 --- /dev/null +++ b/database/migrations/2026_03_23_163000_alter_vip_customers_table.php @@ -0,0 +1,31 @@ +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']); + }); + } +}; + diff --git a/database/migrations/2026_03_23_170000_alter_gate_logs_add_visit_fields.php b/database/migrations/2026_03_23_170000_alter_gate_logs_add_visit_fields.php new file mode 100644 index 0000000..b64d90e --- /dev/null +++ b/database/migrations/2026_03_23_170000_alter_gate_logs_add_visit_fields.php @@ -0,0 +1,33 @@ +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']); + }); + } +}; + diff --git a/database/migrations/2026_03_25_120000_add_watch_only_to_study_logs_table.php b/database/migrations/2026_03_25_120000_add_watch_only_to_study_logs_table.php new file mode 100644 index 0000000..96dd1d5 --- /dev/null +++ b/database/migrations/2026_03_25_120000_add_watch_only_to_study_logs_table.php @@ -0,0 +1,22 @@ +boolean('watch_only')->default(0)->after('type')->comment('1仅观看不答题(VIP)'); + }); + } + + public function down() + { + Schema::table('study_logs', function (Blueprint $table) { + $table->dropColumn('watch_only'); + }); + } +}; diff --git a/routes/api.php b/routes/api.php index e24b59b..81702f6 100644 --- a/routes/api.php +++ b/routes/api.php @@ -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']); diff --git a/scripts/dump-bd-fangke-ali251-to-file.sh b/scripts/dump-bd-fangke-ali251-to-file.sh new file mode 100755 index 0000000..cf590fb --- /dev/null +++ b/scripts/dump-bd-fangke-ali251-to-file.sh @@ -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" diff --git a/scripts/migrate-db-to-yxbd-fangke-ali2.sh b/scripts/migrate-db-to-yxbd-fangke-ali2.sh new file mode 100755 index 0000000..63ec2be --- /dev/null +++ b/scripts/migrate-db-to-yxbd-fangke-ali2.sh @@ -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}."