model = new \app\admin\model\Order; $this->view->assign("priceTypeList", $this->model->getPriceTypeList()); $this->view->assign("statusList", $this->model->getStatusList()); $this->view->assign("payStatusList", $this->model->getPayStatusList()); $this->view->assign("settleStatusList", $this->model->getSettleStatusList()); $this->view->assign("isReviewList", $this->model->getIsReviewList()); $this->view->assign("hasElevatorList", $this->model->getHasElevatorList()); $this->view->assign("urgentList", $this->model->getUrgentList()); } public function index() { $this->request->filter(['strip_tags', 'trim']); if (false === $this->request->isAjax()) { return $this->view->fetch(); } if ($this->request->request('keyField')) { return $this->selectpage(); } [$where, $sort, $order, $offset, $limit] = $this->buildparams(); $list = $this->model ->with(['user', 'master']) ->where($where) ->order($sort, $order) ->paginate($limit); $rows = $list->items(); if (!empty($rows)) { $orderIds = array_column($rows, 'id'); $merLinks = Db::name('merchant_order') ->alias('mo') ->join('merchant m', 'm.id = mo.merchant_id', 'LEFT') ->where('mo.order_id', 'in', $orderIds) ->field('mo.order_id, mo.batch_no, m.name as merchant_name') ->select(); $merMap = []; foreach ($merLinks as $ml) { $merMap[(int)$ml['order_id']] = $ml; } foreach ($rows as &$r) { $oid = (int)$r['id']; $r['merchant_name'] = isset($merMap[$oid]) ? (string)$merMap[$oid]['merchant_name'] : ''; $r['batch_no'] = isset($merMap[$oid]) ? (string)$merMap[$oid]['batch_no'] : ''; } unset($r); } return json(['total' => $list->total(), 'rows' => $rows]); } /** * 禁止后台手工凭空新增订单 */ public function add() { $this->error('订单须由客户端或企业商家工作台真实发起,不支持后台手工伪造'); } /** * 禁止物理删除业务订单 */ public function del($ids = "") { $this->error('订单关联资金流水与结算凭证,禁止物理删除;如需终止履约请编辑取消工单'); } /** * 编辑订单(支持后台派单/改派师傅通知与轨迹联动,以及取消订单自动退回优惠券) */ public function edit($ids = null) { $row = $this->model->with(['user', 'master'])->where('order.id', (int)$ids)->find(); if (!$row) { $this->error(__('No Results were found')); } if (false === $this->request->isPost()) { $catName = (int)$row['cat_id'] > 0 ? (string)Db::name('service_cat')->where('id', (int)$row['cat_id'])->value('name') : ''; $this->view->assign('catName', $catName); $this->view->assign('row', $row); return $this->view->fetch(); } $row = $this->model->get($ids); $params = $this->request->post('row/a', []); if (!$params) { $this->error(__('Parameter %s can not be empty', '')); } $params = $this->preExcludeFields($params); // 锁定订单归属与财务结算核心字段,禁止后台编辑工单时意外篡改账务 unset( $params['order_no'], $params['user_id'], $params['cat_id'], $params['service_id'], $params['price_type'], $params['base_amount'], $params['distance_amount'], $params['floor_amount'], $params['diff_amount'], $params['discount_amount'], $params['total_amount'], $params['pay_amount'], $params['pay_status'], $params['pay_type'], $params['pay_time'], $params['settle_status'], $params['settle_amount'], $params['coupon_id'], $params['verify_code'] ); // 统一转换时间节点字段(留空转为 0,日期字符串转为 Unix 时间戳) foreach (['appoint_time', 'accept_time', 'arrive_time', 'start_time', 'finish_time'] as $tField) { if (array_key_exists($tField, $params)) { $tv = trim((string)$params[$tField]); $params[$tField] = $tv === '' ? 0 : (is_numeric($tv) ? (int)$tv : ((int)strtotime($tv) ?: 0)); } } if (array_key_exists('master_id', $params) && trim((string)$params['master_id']) === '') { $params['master_id'] = 0; } $oldMasterId = (int)$row['master_id']; $oldStatus = (int)$row['status']; $newMasterId = isset($params['master_id']) ? (int)$params['master_id'] : $oldMasterId; $newStatus = isset($params['status']) ? (int)$params['status'] : $oldStatus; $now = time(); // 若 city-picker 提交空串而原库有城市,保留原城市 if (isset($params['city']) && trim((string)$params['city']) === '' && !empty($row['city'])) { unset($params['city']); } $targetMaster = null; if ($newMasterId > 0 && $newMasterId !== $oldMasterId && $newStatus !== 9) { $targetMaster = Db::name('master')->where('id', $newMasterId)->find(); if (!$targetMaster || $targetMaster['verify_status'] !== 'pass') { $this->error('所选承接师傅不存在或尚未通过实名认证'); } if (in_array($newStatus, [1, 2], true)) { $newStatus = 3; $params['status'] = 3; } if (empty($params['accept_time']) || (string)$params['accept_time'] === (string)$row['accept_time']) { $params['accept_time'] = $now; } } $result = false; Db::startTrans(); try { $result = $row->allowField(true)->save($params); // 1. 后台通过编辑指派或改派了师傅:写轨迹日志与三方站内信 if ($targetMaster && $newMasterId !== $oldMasterId && $newStatus !== 9) { $actionLabel = $oldMasterId > 0 ? '平台编辑改派师傅' : '平台编辑指派师傅'; $logRemark = $actionLabel . ':指派给【' . $targetMaster['realname'] . '(' . $targetMaster['mobile'] . ')】'; Db::name('order_log')->insert([ 'order_id' => (int)$row['id'], 'status' => $newStatus, 'remark' => mb_substr($logRemark, 0, 255), 'operator_type' => 'admin', 'operator_id' => (int)$this->auth->id, 'createtime' => $now, ]); Db::name('message')->insert([ 'receive_type' => 'master', 'receive_id' => $newMasterId, 'type' => 'order', 'title' => '平台派单通知', 'content' => '平台已为您指派工单【' . $row['order_no'] . '】(' . $row['service_name'] . '),请及时联系客户预约上门。', 'order_id' => (int)$row['id'], 'is_read' => 0, 'createtime' => $now, ]); if ($oldMasterId > 0 && $oldMasterId !== $newMasterId) { Db::name('message')->insert([ 'receive_type' => 'master', 'receive_id' => $oldMasterId, 'type' => 'order', 'title' => '工单改派通知', 'content' => '工单【' . $row['order_no'] . '】已由平台调度改派给其他师傅承接。', 'order_id' => (int)$row['id'], 'is_read' => 0, 'createtime' => $now, ]); } if ((int)$row['user_id'] > 0) { Db::name('message')->insert([ 'receive_type' => 'user', 'receive_id' => (int)$row['user_id'], 'type' => 'order', 'title' => '师傅指派成功通知', 'content' => '您的订单【' . $row['order_no'] . '】已安排' . $targetMaster['realname'] . '师傅(' . $targetMaster['mobile'] . ')为您服务。', 'order_id' => (int)$row['id'], 'is_read' => 0, 'createtime' => $now, ]); } } // 2. 后台将订单变更为取消状态(status=9):自动退回已锁定的优惠券并记录轨迹通知 if ($oldStatus !== 9 && $newStatus === 9) { $this->refundLockedCoupon((int)$row['id'], (int)$row['coupon_id'], $now); $cancelReason = trim((string)($params['cancel_reason'] ?? $row['cancel_reason'] ?? '')); Db::name('order_log')->insert([ 'order_id' => (int)$row['id'], 'status' => 9, 'remark' => mb_substr('平台运营后台取消工单' . ($cancelReason !== '' ? (':' . $cancelReason) : ''), 0, 255), 'operator_type' => 'admin', 'operator_id' => (int)$this->auth->id, 'createtime' => $now, ]); if ($oldMasterId > 0) { Db::name('message')->insert([ 'receive_type' => 'master', 'receive_id' => $oldMasterId, 'type' => 'order', 'title' => '工单已取消通知', 'content' => '工单【' . $row['order_no'] . '】已由平台取消' . ($cancelReason !== '' ? (',原因:' . $cancelReason) : '') . '。', 'order_id' => (int)$row['id'], 'is_read' => 0, 'createtime' => $now, ]); } if ((int)$row['user_id'] > 0) { Db::name('message')->insert([ 'receive_type' => 'user', 'receive_id' => (int)$row['user_id'], 'type' => 'order', 'title' => '订单取消通知', 'content' => '您的订单【' . $row['order_no'] . '】已取消' . ($cancelReason !== '' ? (',原因:' . $cancelReason) : '') . ',如使用了优惠券已自动退回账户。', 'order_id' => (int)$row['id'], 'is_read' => 0, 'createtime' => $now, ]); } } elseif ($oldStatus !== $newStatus && !($targetMaster && $newMasterId !== $oldMasterId)) { $statusMap = $this->model->getStatusList(); Db::name('order_log')->insert([ 'order_id' => (int)$row['id'], 'status' => $newStatus, 'remark' => '平台运营变更工单状态为:' . ($statusMap[$newStatus] ?? ('状态' . $newStatus)), 'operator_type' => 'admin', 'operator_id' => (int)$this->auth->id, 'createtime' => $now, ]); } Db::commit(); } catch (Exception $e) { Db::rollback(); $this->error($e->getMessage()); } if ($result !== false) { $this->success(); } $this->error(__('No rows were updated')); } /** * 订单取消时退回已锁定的用户优惠券 */ protected function refundLockedCoupon($orderId, $userCouponId, $now) { $ucRows = Db::name('user_coupon') ->where(function ($q) use ($orderId, $userCouponId) { if ($userCouponId > 0) { $q->where('id', $userCouponId)->whereOr('order_id', $orderId); } else { $q->where('order_id', $orderId); } }) ->where('status', 'used') ->select(); foreach ($ucRows as $uc) { $couponTpl = Db::name('coupon')->where('id', (int)$uc['coupon_id'])->find(); $isExpired = $couponTpl && (int)$couponTpl['end_time'] > 0 && (int)$couponTpl['end_time'] < $now; Db::name('user_coupon')->where('id', (int)$uc['id'])->update([ 'status' => $isExpired ? 'expired' : 'unused', 'order_id' => 0, 'use_time' => null, ]); } } /** * 人工指派 / 改派师傅 */ public function dispatch($ids = null) { $row = $this->model->with(['user', 'master'])->where('order.id', (int)$ids)->find(); if (!$row) { $this->error(__('No Results were found')); } if ($this->request->isPost()) { if (in_array((int)$row['status'], [8, 9], true)) { $this->error('已完成或已取消的工单不支持指派/改派'); } if ((int)$row['pay_status'] !== 1 && (int)$row['status'] === 0) { $this->error('该工单尚未完成支付,无法派单'); } $masterId = (int)$this->request->post('master_id', 0); $remark = trim((string)$this->request->post('dispatch_remark', '')); if ($masterId <= 0) { $this->error('请选择要指派的认证师傅'); } $targetMaster = Db::name('master')->where('id', $masterId)->find(); if (!$targetMaster || $targetMaster['verify_status'] !== 'pass') { $this->error('所选师傅不存在或未通过实名认证'); } $oldMasterId = (int)$row['master_id']; if ($oldMasterId === $masterId && (int)$row['status'] >= 3) { $this->error('当前工单已由该师傅承接,无需重复指派'); } $now = time(); $newStatus = in_array((int)$row['status'], [1, 2], true) ? 3 : (int)$row['status']; $actionLabel = $oldMasterId > 0 ? '平台改派师傅' : '平台人工指派师傅'; $logRemark = $actionLabel . ':指派给【' . $targetMaster['realname'] . '(' . $targetMaster['mobile'] . ')】' . ($remark !== '' ? (',备注:' . $remark) : ''); Db::startTrans(); try { $updateData = [ 'master_id' => $masterId, 'status' => $newStatus, 'updatetime' => $now, ]; if (empty($row['accept_time']) || $oldMasterId !== $masterId) { $updateData['accept_time'] = $now; } Db::name('order')->where('id', $row['id'])->update($updateData); Db::name('order_log')->insert([ 'order_id' => $row['id'], 'status' => $newStatus, 'remark' => mb_substr($logRemark, 0, 255), 'operator_type' => 'admin', 'operator_id' => (int)$this->auth->id, 'createtime' => $now, ]); // 通知新接单师傅 Db::name('message')->insert([ 'receive_type' => 'master', 'receive_id' => $masterId, 'type' => 'order', 'title' => '平台派单通知', 'content' => '平台已为您指派工单【' . $row['order_no'] . '】(' . $row['service_name'] . '),请及时联系客户预约上门。', 'order_id' => (int)$row['id'], 'is_read' => 0, 'createtime' => $now, ]); // 若为改派,通知原师傅 if ($oldMasterId > 0 && $oldMasterId !== $masterId) { Db::name('message')->insert([ 'receive_type' => 'master', 'receive_id' => $oldMasterId, 'type' => 'order', 'title' => '工单改派通知', 'content' => '工单【' . $row['order_no'] . '】已由平台调度改派给其他师傅承接。', 'order_id' => (int)$row['id'], 'is_read' => 0, 'createtime' => $now, ]); } // 通知下单业主 if ((int)$row['user_id'] > 0) { Db::name('message')->insert([ 'receive_type' => 'user', 'receive_id' => (int)$row['user_id'], 'type' => 'order', 'title' => '师傅指派成功通知', 'content' => '您的订单【' . $row['order_no'] . '】已安排' . $targetMaster['realname'] . '师傅(' . $targetMaster['mobile'] . ')为您服务。', 'order_id' => (int)$row['id'], 'is_read' => 0, 'createtime' => $now, ]); } Db::commit(); } catch (Exception $e) { Db::rollback(); $this->error($e->getMessage()); } $this->success('派单调度成功'); } // 推荐候选师傅:优先同城在线且实名通过的师傅 $cityKeyword = ''; if (!empty($row['city'])) { $parts = explode('/', (string)$row['city']); $cityKeyword = trim((string)(count($parts) > 1 ? $parts[1] : $parts[0])); $cityKeyword = preg_replace('/市$/u', '', $cityKeyword); } $candidateMasters = Db::name('master') ->where('verify_status', 'pass') ->orderRaw("work_status = 'on' DESC, score DESC, order_count DESC") ->limit(12) ->select(); if ($cityKeyword !== '') { $sameCity = []; $otherCity = []; foreach ($candidateMasters as $cm) { if (mb_strpos((string)$cm['city'], $cityKeyword) !== false) { $sameCity[] = $cm; } else { $otherCity[] = $cm; } } $candidateMasters = array_slice(array_merge($sameCity, $otherCity), 0, 8); } else { $candidateMasters = array_slice($candidateMasters, 0, 8); } $cmIds = array_column($candidateMasters, 'id'); $cmSkillMap = []; if (!empty($cmIds)) { $skRows = Db::name('master_skill')->where('master_id', 'in', $cmIds)->where('status', 'normal')->select(); foreach ($skRows as $sk) { if (!empty($sk['skill_name'])) { $cmSkillMap[(int)$sk['master_id']][] = $sk['skill_name']; } } } foreach ($candidateMasters as &$cm) { $cm['skills'] = array_slice($cmSkillMap[(int)$cm['id']] ?? [], 0, 3); } unset($cm); $this->view->assign('row', $row); $this->view->assign('candidateMasters', $candidateMasters); return $this->view->fetch(); } /** * 工单全景详情 */ public function detail($ids = null) { $row = $this->model->with(['user', 'master'])->where('order.id', (int)$ids)->find(); if (!$row) { $this->error(__('No Results were found')); } // 1. 多商品/多工项明细 $goodsList = Db::name('order_goods')->where('order_id', $row['id'])->select(); // 2. 现场施工取证图片 (区分签到、过程、完工、报损) $mediaList = Db::name('order_media')->where('order_id', $row['id'])->order('id asc')->select(); // 3. 履约轨迹日志 $statusMap = $this->model->getStatusList(); $logList = Db::name('order_log')->where('order_id', $row['id'])->order('createtime asc, id asc')->select(); foreach ($logList as &$lg) { $lg['status_label'] = isset($statusMap[(int)$lg['status']]) ? $statusMap[(int)$lg['status']] : '状态变更'; } unset($lg); // 4. 师傅报价列表 $quoteList = Db::name('order_quote') ->alias('q') ->join('master m', 'q.master_id = m.id', 'LEFT') ->field('q.*, m.realname as master_name, m.mobile as master_mobile, m.score as master_score') ->where('q.order_id', $row['id']) ->order('q.id desc') ->select(); // 5. 客户评价与回复(兼容 JSON 数组或逗号分隔标签) $review = Db::name('review')->where('order_id', $row['id'])->find(); if ($review) { $tagsArr = []; if (!empty($review['tags'])) { $decoded = json_decode($review['tags'], true); if (is_array($decoded)) { $tagsArr = array_filter($decoded); } else { $tagsArr = array_filter(explode(',', (string)$review['tags'])); } } $review['tags_arr'] = $tagsArr; } // 6. 财务结算记录 $settle = Db::name('settle')->where('order_id', $row['id'])->find(); // 7. 企业商家协同发包来源 $merchantOrder = Db::name('merchant_order') ->alias('mo') ->join('merchant m', 'm.id = mo.merchant_id', 'LEFT') ->where('mo.order_id', $row['id']) ->field('mo.batch_no, mo.createtime as bind_time, m.id as merchant_id, m.name as merchant_name, m.contact as merchant_contact, m.mobile as merchant_mobile, m.settle_type') ->find(); // 8. 发票开具关联信息 $invoiceInfo = null; if (!empty($row['invoice_id'])) { $invoiceInfo = Db::name('invoice') ->field('id,invoice_sn,title,type,amount,status,invoice_no,issue_time') ->where('id', (int)$row['invoice_id']) ->find(); } // 9. 售后申诉争议记录 $appealList = Db::name('appeal') ->alias('a') ->join('master m', 'm.id = a.master_id', 'LEFT') ->where('a.order_id', $row['id']) ->field('a.*, m.realname as master_name') ->order('a.id desc') ->select(); $this->view->assign([ 'row' => $row, 'goodsList' => $goodsList, 'mediaList' => $mediaList, 'logList' => $logList, 'quoteList' => $quoteList, 'review' => $review, 'settle' => $settle, 'merchantOrder' => $merchantOrder, 'invoiceInfo' => $invoiceInfo, 'appealList' => $appealList, ]); return $this->view->fetch(); } }