trim((string)$this->request->post('realname')), 'mobile' => trim((string)$this->request->post('mobile')), 'idcard_no' => trim((string)$this->request->post('idcard_no')), 'idcard_front' => (string)$this->request->post('idcard_front'), 'idcard_back' => (string)$this->request->post('idcard_back'), 'cert_images' => (string)$this->request->post('cert_images'), 'city' => trim((string)$this->request->post('city')), 'skills' => (array)$this->request->post('skills/a', []), ]; if ($data['realname'] === '' || $data['mobile'] === '' || $data['idcard_no'] === '') { $this->error('请填写真实姓名、手机号和身份证号'); } if (!preg_match('/^1\d{10}$/', $data['mobile'])) { $this->error('手机号格式不正确'); } if (empty($data['skills'])) { $this->error('请至少选择一个服务类目'); } $exist = Db::name('master')->where('user_id', $this->uid())->find(); if ($exist && $exist['verify_status'] === 'pass') { $this->error('您已是认证师傅,无需重复入驻'); } $save = [ 'realname' => $data['realname'], 'mobile' => $data['mobile'], 'idcard_no' => substr($data['idcard_no'], 0, 6) . '********' . substr($data['idcard_no'], -4), 'idcard_front' => $data['idcard_front'], 'idcard_back' => $data['idcard_back'], 'cert_images' => $data['cert_images'], 'city' => $data['city'], 'verify_status' => 'pending', 'audit_msg' => '', 'updatetime' => time(), ]; Db::startTrans(); try { if ($exist) { $masterId = (int)$exist['id']; Db::name('master')->where('id', $masterId)->update($save); Db::name('master_skill')->where('master_id', $masterId)->delete(); } else { $save['user_id'] = $this->uid(); $save['createtime'] = time(); $masterId = Db::name('master')->insertGetId($save); } foreach ($data['skills'] as $catId) { Db::name('master_skill')->insert([ 'master_id' => $masterId, 'cat_id' => (int)$catId, 'status' => 'normal', 'createtime' => time(), ]); } Db::commit(); } catch (\Exception $e) { Db::rollback(); $this->error('提交失败:' . $e->getMessage()); } $this->success('提交成功,请等待审核', ['master_id' => (int)$masterId]); } /** * 我的师傅档案 */ public function info() { $master = $this->masterInfo(); if (!$master) { $this->success('未入驻', ['applied' => false]); } $skills = Db::name('master_skill')->alias('s') ->join('service_cat c', 'c.id = s.cat_id', 'LEFT') ->field('s.cat_id,c.name,c.icon') ->where('s.master_id', $master['id']) ->select(); $todo = Db::name('order')->where('master_id', $master['id']) ->where('status', 'in', [OrderStatus::ACCEPTED, OrderStatus::SCHEDULED, OrderStatus::ARRIVED, OrderStatus::WORKING]) ->count(); $pendingVerify = Db::name('order')->where('master_id', $master['id']) ->where('status', OrderStatus::PENDING_VERIFY)->count(); $this->success('', [ 'applied' => true, 'master' => [ 'id' => (int)$master['id'], 'realname' => $master['realname'], 'mobile' => $master['mobile'], 'avatar' => $master['avatar'], 'city' => $master['city'], 'level' => (int)$master['level'], 'score' => (float)$master['score'], 'order_count' => (int)$master['order_count'], 'accept_rate' => (float)$master['accept_rate'], 'ontime_rate' => (float)$master['ontime_rate'], 'work_status' => $master['work_status'], 'verify_status' => $master['verify_status'], 'audit_msg' => $master['audit_msg'], 'money' => (float)$master['money'], 'todo_count' => (int)$todo, 'verify_count' => (int)$pendingVerify, ], 'skills' => $skills, ]); } /** * 接单开关 * @ApiMethod (POST) */ public function toggleWork() { $master = $this->requireMaster(); $status = $this->request->post('work_status', ''); if (!in_array($status, ['on', 'off'], true)) { $this->error('参数错误'); } Db::name('master')->where('id', $master['id'])->update(['work_status' => $status, 'updatetime' => time()]); $this->success($status === 'on' ? '已开启接单' : '已暂停接单', ['work_status' => $status]); } /** * 订单大厅:支持排序与单类型筛选(一口价 / 师傅报价 / 用户自主定价) */ public function hall() { $master = $this->requireMaster(); $this->recycleExpired(); $catIds = Db::name('master_skill')->where('master_id', $master['id'])->column('cat_id'); $page = max(1, (int)$this->request->param('page', 1)); $limit = min(30, max(1, (int)$this->request->param('limit', 10))); $sort = $this->request->param('sort', 'new'); // new 最新 / amount 报酬 / urgent 急单 / time 时效 $type = $this->request->param('type', 'all'); // all / fixed 一口价 / quote 师傅报价 / custom 用户自主定价 $build = function () use ($catIds, $type) { $q = Db::name('order')->where('master_id', 0) ->where('status', 'in', [OrderStatus::PENDING_ACCEPT, OrderStatus::QUOTING]); if (!empty($catIds)) { $q->where('cat_id', 'in', $catIds); } if ($type === 'fixed') { $q->where('status', OrderStatus::PENDING_ACCEPT)->where('price_type', 'fixed'); } elseif ($type === 'quote') { $q->where('status', 'in', [OrderStatus::PENDING_ACCEPT, OrderStatus::QUOTING])->where('price_type', 'quote'); } elseif ($type === 'custom') { $q->where('status', OrderStatus::PENDING_ACCEPT)->where('price_type', 'custom'); } return $q; }; if ($sort === 'amount') { $order = 'pay_amount desc, id desc'; } elseif ($sort === 'urgent') { $order = 'urgent desc, id desc'; } elseif ($sort === 'time') { $order = 'appoint_time asc, id desc'; } else { $order = 'id desc'; } $total = $build()->count(); $list = $build()->order($order)->page($page, $limit)->select(); $now = time(); foreach ($list as &$row) { $row['appoint_text'] = $row['appoint_time'] ? date('m-d H:i', (int)$row['appoint_time']) : '尽快上门'; $row['pay_amount'] = (float)$row['pay_amount']; $row['urgent'] = (int)$row['urgent']; $row['mobile'] = substr_replace($row['mobile'], '****', 3, 4); $row['address'] = $row['city'] . $row['district'] . $row['address']; $row['status_text'] = OrderStatus::label($row['status']); $row['left_seconds'] = max(0, (int)$row['createtime'] + 3600 - $now); $row['quote_count'] = (int)Db::name('order_quote')->where('order_id', $row['id'])->count(); $row['price_type_text'] = $row['price_type'] === 'fixed' ? '一口价' : ($row['price_type'] === 'quote' ? '师傅报价' : ($row['price_type'] === 'custom' ? '用户自主定价' : '按面积计价')); $row['user_price'] = $row['price_type'] === 'custom' ? (float)$row['base_amount'] : 0; } unset($row); $this->success('', [ 'list' => $list, 'total' => (int)$total, 'page' => $page, 'sort' => $sort, 'type' => $type, ]); } /** * 抢单(条件更新保证并发安全) * @ApiMethod (POST) */ public function grab() { $master = $this->requireMaster(); if ($master['work_status'] !== 'on') { $this->error('您已暂停接单,请先开启'); } $orderId = (int)$this->request->post('order_id'); $order = Db::name('order')->where('id', $orderId)->find(); if (!$order) { $this->error('订单不存在'); } if ($order['price_type'] === 'quote') { $this->error('该订单为师傅报价订单,请提交报价'); } if ((int)$order['status'] !== OrderStatus::PENDING_ACCEPT || (int)$order['master_id'] !== 0) { $this->error('手慢了,该订单已被接走'); } $affected = Db::name('order') ->where('id', $orderId) ->where('status', OrderStatus::PENDING_ACCEPT) ->where('master_id', 0) ->update([ 'master_id' => (int)$master['id'], 'status' => OrderStatus::ACCEPTED, 'accept_time' => time(), 'updatetime' => time(), ]); if (!$affected) { $this->error('手慢了,该订单已被接走'); } $this->addLog($orderId, OrderStatus::ACCEPTED, '师傅接单:' . $master['realname'], 'master', (int)$master['id']); $this->notify('user', (int)$order['user_id'], '师傅已接单', '师傅 ' . $master['realname'] . ' 已接单,将尽快与您联系', 'order', $orderId); $this->success('抢单成功,请在 2 小时内联系客户', [ 'order_id' => $orderId, 'deadline' => time() + 7200, 'user_mobile' => $order['mobile'], ]); } /** * 标记已联系客户(2 小时考核) * @ApiMethod (POST) */ public function contact() { $order = $this->ownOrder((int)$this->request->post('order_id'), 'master'); if ((int)$order['contact_time'] > 0) { $this->success('已记录'); } Db::name('order')->where('id', $order['id'])->update(['contact_time' => time(), 'updatetime' => time()]); $this->addLog((int)$order['id'], (int)$order['status'], '师傅已联系客户', 'master', (int)$order['master_id']); $this->success('已记录联系时间'); } /** * 设置预约上门时间 * @ApiMethod (POST) */ public function schedule() { $order = $this->ownOrder((int)$this->request->post('order_id'), 'master'); $appointTime = (int)$this->request->post('appoint_time'); if ($appointTime <= 0) { $this->error('请选择上门时间'); } if ((int)$order['status'] === OrderStatus::ACCEPTED) { $this->transit($order, OrderStatus::SCHEDULED, '已与客户约定上门时间', [ 'appoint_time' => $appointTime, 'contact_time' => (int)$order['contact_time'] ?: time(), ], 'master', (int)$order['master_id']); } else { Db::name('order')->where('id', $order['id'])->update(['appoint_time' => $appointTime, 'updatetime' => time()]); } $this->notify('user', (int)$order['user_id'], '师傅已预约上门', '约定时间:' . date('m-d H:i', $appointTime), 'order', (int)$order['id']); $this->success('预约成功'); } /** * 到达现场(定位打卡) * @ApiMethod (POST) */ public function arrive() { $order = $this->ownOrder((int)$this->request->post('order_id'), 'master'); $lng = (string)$this->request->post('lng', ''); $lat = (string)$this->request->post('lat', ''); $extra = ['arrive_time' => time()]; if ($lng !== '' && $lat !== '') { $extra['lng'] = $lng; $extra['lat'] = $lat; } $this->transit($order, OrderStatus::ARRIVED, '师傅已到达现场', $extra, 'master', (int)$order['master_id']); $this->notify('user', (int)$order['user_id'], '师傅已到达', '师傅已到达服务地址,请做好准备', 'order', (int)$order['id']); $this->success('已打卡到达'); } /** * 开始服务 * @ApiMethod (POST) */ public function startJob() { $order = $this->ownOrder((int)$this->request->post('order_id'), 'master'); $this->transit($order, OrderStatus::WORKING, '师傅开始施工', ['start_time' => time()], 'master', (int)$order['master_id']); $this->notify('user', (int)$order['user_id'], '师傅已开工', '师傅已开始服务,请留意进度', 'order', (int)$order['id']); $this->success('已开工'); } /** * 完工并提交现场取证 → 进入待验收 * @ApiMethod (POST) */ public function finish() { $order = $this->ownOrder((int)$this->request->post('order_id'), 'master'); $images = (array)$this->request->post('images/a', []); $damage = trim((string)$this->request->post('damage_remark', '')); Db::startTrans(); try { $this->transit($order, OrderStatus::PENDING_VERIFY, '师傅完工,等待客户验收', [ 'finish_time' => time(), ], 'master', (int)$order['master_id']); foreach ($images as $url) { if (!$url) { continue; } Db::name('order_media')->insert([ 'order_id' => (int)$order['id'], 'master_id' => (int)$order['master_id'], 'stage' => $damage !== '' ? 'damage' : 'finish', 'type' => 'image', 'url' => (string)$url, 'watermark' => date('Y-m-d H:i') . ' ' . $order['address'], 'remark' => $damage !== '' ? mb_substr($damage, 0, 255) : '', 'createtime' => time(), ]); } Db::commit(); } catch (\Exception $e) { Db::rollback(); $this->error('提交失败:' . $e->getMessage()); } $code = (string)$order['verify_code']; $this->notify('user', (int)$order['user_id'], '师傅已完工,待您验收', '验收码:' . $code . ',请核对后确认完成', 'order', (int)$order['id']); $this->success('已提交完工,等待客户验收', ['verify_code' => $code]); } /** * 我的订单(师傅视角) */ public function orders() { $master = $this->requireMaster(); $status = $this->request->param('status', 'active'); $page = max(1, (int)$this->request->param('page', 1)); $limit = min(30, max(1, (int)$this->request->param('limit', 10))); // TP5 中 count() 会重置查询条件,故用闭包重建 $build = function () use ($master, $status) { $q = Db::name('order')->where('master_id', $master['id']); if ($status === 'active') { $q->where('status', 'in', [OrderStatus::ACCEPTED, OrderStatus::SCHEDULED, OrderStatus::ARRIVED, OrderStatus::WORKING]); } elseif ($status === 'verify') { $q->where('status', OrderStatus::PENDING_VERIFY); } elseif ($status === 'completed') { $q->where('status', OrderStatus::COMPLETED); } return $q; }; $total = $build()->count(); $list = $build()->order('id desc')->page($page, $limit)->select(); foreach ($list as &$row) { $row['status_text'] = OrderStatus::label($row['status']); $row['pay_amount'] = (float)$row['pay_amount']; $row['appoint_text'] = $row['appoint_time'] ? date('m-d H:i', (int)$row['appoint_time']) : '尽快上门'; $row['urgent'] = (int)$row['urgent']; $row['masked_mobile'] = substr_replace($row['mobile'], '****', 3, 4); } unset($row); $this->success('', ['list' => $list, 'total' => (int)$total, 'page' => $page]); } /** * 收入概览 */ public function income() { $master = $this->requireMaster(); $todayStart = strtotime(date('Y-m-d')); $todayIncome = Db::name('wallet_log')->where('user_type', 'master')->where('user_id', $master['id']) ->where('type', 'settle')->where('createtime', '>=', $todayStart)->sum('amount'); $totalIncome = Db::name('wallet_log')->where('user_type', 'master')->where('user_id', $master['id']) ->where('type', 'settle')->sum('amount'); $logs = Db::name('wallet_log')->where('user_type', 'master')->where('user_id', $master['id']) ->order('id desc')->limit(20)->select(); $this->success('', [ 'money' => (float)$master['money'], 'frozen_money' => (float)$master['frozen_money'], 'today_income' => round((float)$todayIncome, 2), 'total_income' => round((float)$totalIncome, 2), 'order_count' => (int)$master['order_count'], 'logs' => $logs, ]); } /** * 报价(报价竞价单) * @ApiMethod (POST) */ public function quote() { $master = $this->requireMaster(); $orderId = (int)$this->request->post('order_id'); $amount = (float)$this->request->post('amount'); $desc = trim((string)$this->request->post('description', '')); $hours = min(72, max(1, (int)$this->request->post('valid_hours', 24))); $order = Db::name('order')->where('id', $orderId)->find(); if (!$order) { $this->error('订单不存在'); } if ($order['price_type'] !== 'quote') { $this->error('该订单不是报价竞价订单'); } if (!in_array((int)$order['status'], [OrderStatus::PENDING_ACCEPT, OrderStatus::QUOTING], true)) { $this->error('该订单已不在报价阶段'); } if ((int)$order['master_id'] > 0) { $this->error('该订单已选定师傅'); } if ($amount <= 0) { $this->error('请填写报价金额'); } if (Db::name('order_quote')->where('order_id', $orderId)->where('master_id', $master['id'])->find()) { $this->error('您已报价,请勿重复提交'); } Db::name('order_quote')->insert([ 'order_id' => $orderId, 'master_id' => (int)$master['id'], 'amount' => round($amount, 2), 'description' => mb_substr($desc, 0, 500), 'valid_hours' => $hours, 'expire_time' => time() + $hours * 3600, 'status' => 'wait', 'createtime' => time(), ]); // 兼容旧流程:已支付但仍在待接单的报价单,收到首个报价时进入「报价中」 if ((int)$order['status'] === OrderStatus::PENDING_ACCEPT) { $this->transit($order, OrderStatus::QUOTING, '收到师傅报价,订单进入报价中', [], 'master', (int)$master['id']); } $this->notify('user', (int)$order['user_id'], '收到新报价', '师傅 ' . $master['realname'] . ' 对订单 ' . $order['order_no'] . ' 报价 ¥' . round($amount, 2) . ',请前往订单详情选择', 'order', $orderId); $this->success('报价已提交'); } /** * 申请提现 * @ApiMethod (POST) */ public function withdraw() { $master = $this->requireMaster(); $amount = round((float)$this->request->post('amount'), 2); if ($amount < 10) { $this->error('单次提现金额不少于 10 元'); } if ($amount > (float)$master['money']) { $this->error('可提现余额不足'); } $fee = round($amount * 0.006, 2); $arrive = round($amount - $fee, 2); Db::startTrans(); try { Db::name('withdraw')->insert([ 'master_id' => (int)$master['id'], 'amount' => $amount, 'fee' => $fee, 'arrive_amount' => $arrive, 'channel' => $this->request->post('channel', 'wechat'), 'account' => (string)$this->request->post('account', ''), 'status' => 'wait', 'createtime' => time(), 'updatetime' => time(), ]); $newMoney = round((float)$master['money'] - $amount, 2); Db::name('master')->where('id', $master['id'])->update([ 'money' => $newMoney, 'frozen_money' => round((float)$master['frozen_money'] + $amount, 2), 'updatetime' => time(), ]); Db::name('wallet_log')->insert([ 'user_type' => 'master', 'user_id' => (int)$master['id'], 'type' => 'withdraw', 'amount' => -$amount, 'balance' => $newMoney, 'order_id' => 0, 'remark' => '提现申请(手续费 ¥' . $fee . ')', 'createtime' => time(), ]); Db::commit(); } catch (\Exception $e) { Db::rollback(); $this->error('提现申请失败:' . $e->getMessage()); } $this->success('提现申请已提交,等待审核', ['arrive_amount' => $arrive]); } /** * 超 2 小时未联系客户 → 订单自动回流大厅 */ protected function recycleExpired() { $rows = Db::name('order') ->where('status', OrderStatus::ACCEPTED) ->where('contact_time', 0) ->where('accept_time', '<', time() - 7200) ->limit(50) ->select(); foreach ($rows as $row) { $toStatus = $row['price_type'] === 'quote' ? OrderStatus::QUOTING : OrderStatus::PENDING_ACCEPT; Db::name('order')->where('id', $row['id'])->update([ 'master_id' => 0, 'status' => $toStatus, 'accept_time' => null, 'updatetime' => time(), ]); if ($row['price_type'] === 'quote') { Db::name('order_quote')->where('order_id', $row['id'])->where('status', 'accept')->update(['status' => 'wait']); } $this->addLog($row['id'], $toStatus, $row['price_type'] === 'quote' ? '师傅超 2 小时未联系客户,订单重新开放报价' : '超 2 小时未联系客户,订单自动回流大厅', 'system', 0); } } /** * 信用体系:综合评分 / 接单率 / 准时率 / 好评率 / 投诉率 */ public function credit() { $master = $this->requireMaster(); $mid = (int)$master['id']; $reviewTotal = (int)Db::name('review')->where('master_id', $mid)->where('status', 'normal')->count(); $reviewGood = (int)Db::name('review')->where('master_id', $mid)->where('status', 'normal')->where('score', '>=', 4)->count(); $appealCount = (int)Db::name('appeal')->where('master_id', $mid)->count(); $orderCount = max(1, (int)$master['order_count']); $this->success('', [ 'score' => (float)$master['score'], 'level' => (int)$master['level'], 'accept_rate' => (float)$master['accept_rate'], 'ontime_rate' => (float)$master['ontime_rate'], 'good_rate' => $reviewTotal ? round($reviewGood * 100 / $reviewTotal, 1) : 100, 'complaint_rate' => round($appealCount * 100 / $orderCount, 1), 'order_count' => (int)$master['order_count'], 'review_total' => $reviewTotal, 'appeal_total' => $appealCount, 'rules' => [ '综合评分 = 客户评价星级平均值(5 分制)', '接单率 = 已联系客户订单 / 接单总数', '准时率 = 按预约时间到达 / 上门总数', '好评率 = 4 星及以上评价占比', '投诉率 = 申诉工单数 / 完成订单数', ], ]); } /** * 申诉中心:列表 */ public function appeals() { $master = $this->requireMaster(); $list = Db::name('appeal')->where('master_id', $master['id'])->order('id desc')->limit(30)->select(); $typeText = ['misjudge' => '误判申诉', 'review' => '恶意评价', 'deduct' => '扣款申诉', 'other' => '其他']; $statusText = ['wait' => '待处理', 'handling' => '处理中', 'done' => '已处理', 'reject' => '已驳回']; foreach ($list as &$row) { $row['type_text'] = isset($typeText[$row['type']]) ? $typeText[$row['type']] : '其他'; $row['status_text'] = isset($statusText[$row['status']]) ? $statusText[$row['status']] : $row['status']; $row['time_text'] = date('Y-m-d H:i', (int)$row['createtime']); } unset($row); $this->success('', ['list' => $list]); } /** * 申诉中心:提交 * @ApiMethod (POST) */ public function appeal() { $master = $this->requireMaster(); $type = $this->request->post('type', 'other'); $orderId = (int)$this->request->post('order_id', 0); $content = trim((string)$this->request->post('content')); if (!in_array($type, ['misjudge', 'review', 'deduct', 'other'], true)) { $this->error('申诉类型错误'); } if (mb_strlen($content) < 5) { $this->error('请填写至少 5 个字的申诉说明'); } if (Db::name('appeal')->where('master_id', $master['id'])->where('status', 'wait')->count() >= 5) { $this->error('您有多个待处理申诉,请等待处理完成'); } Db::name('appeal')->insert([ 'master_id' => $master['id'], 'order_id' => $orderId, 'type' => $type, 'content' => mb_substr($content, 0, 500), 'images' => (string)$this->request->post('images', ''), 'status' => 'wait', 'createtime' => time(), 'updatetime' => time(), ]); $this->success('申诉已提交,平台将在 24 小时内处理'); } /** * 我的报价记录 */ public function quotes() { $master = $this->requireMaster(); $list = Db::name('order_quote')->alias('q') ->join('order o', 'o.id = q.order_id', 'LEFT') ->field('q.*, o.order_no, o.service_name, o.pay_amount as order_amount, o.status as order_status') ->where('q.master_id', $master['id']) ->order('q.id desc') ->limit(30) ->select(); $statusText = ['wait' => '等待客户选择', 'accept' => '已被选中', 'reject' => '未被选中', 'expired' => '已过期']; foreach ($list as &$row) { $row['status_text'] = isset($statusText[$row['status']]) ? $statusText[$row['status']] : $row['status']; $row['amount'] = (float)$row['amount']; $row['time_text'] = date('m-d H:i', (int)$row['createtime']); $row['expire_text'] = date('m-d H:i', (int)$row['expire_time']); } unset($row); $this->success('', ['list' => $list]); } }