success('', ['list' => OrderStatus::all()]); } /** * 下单(创建待支付订单) * @ApiMethod (POST) */ public function create() { // 频次防刷限制:同一用户 3 秒内禁止连续下单,防并发重复创单 $lastOrderTime = Db::name('order')->where('user_id', $this->uid())->order('id desc')->value('createtime'); if ($lastOrderTime && (time() - (int)$lastOrderTime) < 3) { $this->error('操作过于频繁,请稍候再试'); } $serviceId = (int)$this->request->post('service_id'); $addressId = (int)$this->request->post('address_id'); $appointTime = (int)$this->request->post('appoint_time', 0); $urgent = (int)$this->request->post('urgent', 0); $couponId = (int)$this->request->post('coupon_id', 0); $goodsModel = trim((string)$this->request->post('goods_model')); $goodsNum = max(1, (int)$this->request->post('goods_num', 1)); $remark = trim((string)$this->request->post('remark')); $goodsImages = $this->request->post('goods_images', ''); // 支持多商品组合 (goods_list) $goodsListRaw = $this->request->post('goods_list', '', null); $goodsList = []; if (!empty($goodsListRaw)) { if (is_string($goodsListRaw)) { $rawStr = htmlspecialchars_decode($goodsListRaw); $decoded = json_decode($rawStr, true); if (is_array($decoded)) { $goodsList = $decoded; } } elseif (is_array($goodsListRaw)) { if (isset($goodsListRaw[0]) && is_string($goodsListRaw[0])) { $rawStr = htmlspecialchars_decode($goodsListRaw[0]); $decoded = json_decode($rawStr, true); $goodsList = is_array($decoded) ? $decoded : $goodsListRaw; } else { $goodsList = $goodsListRaw; } } } $service = null; if (!empty($goodsList) && is_array($goodsList)) { $first = reset($goodsList); $serviceId = isset($first['service_id']) ? (int)$first['service_id'] : $serviceId; } if ($serviceId > 0) { $service = Db::name('service')->where('id', $serviceId)->where('status', 'normal')->find(); } if (!$service && empty($goodsList)) { $this->error('服务不存在或已下架'); } // 下单页可选择:一口价 / 师傅报价 / 用户自主定价(未传则沿用服务默认) $defaultPriceType = $service ? $service['price_type'] : 'fixed'; $priceType = trim((string)$this->request->post('price_type', $defaultPriceType)); if (!in_array($priceType, ['fixed', 'quote', 'custom', 'range'], true)) { $priceType = $defaultPriceType; } $userPrice = round((float)$this->request->post('user_price', 0), 2); if ($priceType === 'custom') { if ($userPrice < 1 || $userPrice > 100000) { $this->error('请填写 1-100000 元的出价'); } } else { $userPrice = 0; } // 支持直接传 address_id 或传入新地址对象并自动保存 $address = null; if ($addressId > 0) { $address = Db::name('address')->where('id', $addressId)->where('user_id', $this->uid())->find(); } if (!$address) { $contact = trim((string)$this->request->post('contact')); $mobile = trim((string)$this->request->post('mobile')); $province = trim((string)$this->request->post('province', '贵州省')); $city = trim((string)$this->request->post('city', '贵阳市')); $district = trim((string)$this->request->post('district', '')); $detailAddr = trim((string)$this->request->post('address')); $floor = max(1, (int)$this->request->post('floor', 1)); $hasElevator = (int)$this->request->post('has_elevator', 1); if ($contact && $mobile && $detailAddr) { $addressId = Db::name('address')->insertGetId([ 'user_id' => $this->uid(), 'contact' => $contact, 'mobile' => $mobile, 'province' => $province, 'city' => $city, 'district' => $district, 'address' => $detailAddr, 'floor' => $floor, 'has_elevator' => $hasElevator, 'createtime' => time(), 'updatetime' => time(), ]); $address = Db::name('address')->where('id', $addressId)->find(); } } if (!$address) { $this->error('请选择或填写完整服务地址'); } if ($appointTime > 0 && $appointTime < time()) { $this->error('预约时间不能早于当前时间'); } // 多商品组合名称、总件数与图片汇总 $serviceName = $service ? $service['name'] : '同城综合上门服务'; $catId = $service ? (int)$service['cat_id'] : 1; $serviceImage = $service ? $service['image'] : ''; if (!empty($goodsList) && is_array($goodsList)) { $firstG = reset($goodsList); if (!empty($firstG['cat_id'])) { $catId = (int)$firstG['cat_id']; } $names = []; $totalCount = 0; $allImages = []; foreach ($goodsList as $idx => $g) { $gName = !empty($g['custom_name']) ? $g['custom_name'] : (isset($g['item_name']) ? $g['item_name'] : (isset($g['goods_name']) ? $g['goods_name'] : (isset($g['name']) ? $g['name'] : '服务项'))); $gNum = isset($g['num']) ? (int)$g['num'] : 1; $names[] = $gName . 'x' . $gNum; $totalCount += $gNum; if (!empty($g['images']) && is_array($g['images'])) { $allImages = array_merge($allImages, $g['images']); } } if (!empty($names)) { $serviceName = implode(' + ', array_slice($names, 0, 3)) . (count($names) > 3 ? ' 等' : ''); } if ($totalCount > 0) { $goodsNum = $totalCount; } if (!empty($allImages)) { $goodsImages = $allImages; } } // 金额统一由 Home::calcAmount 计算,保证与试算口径一致 $amount = Home::calcAmount($service, [ 'floor' => (int)$address['floor'], 'has_elevator' => (int)$address['has_elevator'], 'urgent' => $urgent, ], $couponId, $this->uid(), $priceType, $userPrice, $goodsList); if (empty($serviceImage) && !empty($goodsList)) { foreach ($goodsList as $item) { if (!empty($item['goods_image'])) { $serviceImage = $item['goods_image']; break; } elseif (!empty($item['images']) && is_array($item['images'])) { $serviceImage = reset($item['images']); break; } elseif (!empty($item['service_id'])) { $subSvc = Db::name('service')->where('id', (int)$item['service_id'])->find(); if ($subSvc && !empty($subSvc['image'])) { $serviceImage = $subSvc['image']; break; } } } } if (empty($serviceImage) && $service && !empty($service['image'])) { $serviceImage = $service['image']; } $amount['price_type'] = $priceType; $orderNo = $this->makeOrderNo(); $verifyCode = (string)mt_rand(1000, 9999); // 如果有多商品明细,结构化存入 goods_images 或在 remark 补充 $goodsImagesPayload = is_array($goodsImages) ? json_encode($goodsImages, JSON_UNESCAPED_UNICODE) : (string)$goodsImages; if (!empty($goodsList)) { $extraPayload = [ 'goods_list' => $goodsList, 'images' => is_array($goodsImages) ? $goodsImages : [], ]; $goodsImagesPayload = json_encode($extraPayload, JSON_UNESCAPED_UNICODE); } $id = Db::name('order')->insertGetId([ 'order_no' => $orderNo, 'user_id' => $this->uid(), 'master_id' => 0, 'cat_id' => $catId, 'service_id' => $service ? (int)$service['id'] : 0, 'service_name' => $serviceName, 'service_image' => $serviceImage, 'price_type' => $priceType, 'goods_images' => $goodsImagesPayload, 'goods_model' => mb_substr($goodsModel, 0, 100), 'goods_num' => $goodsNum, 'floor' => (int)$address['floor'], 'has_elevator' => (int)$address['has_elevator'], 'urgent' => $urgent ? 1 : 0, 'remark' => mb_substr($remark, 0, 500), 'contact' => $address['contact'], 'mobile' => $address['mobile'], 'province' => $address['province'], 'city' => $address['city'], 'district' => $address['district'], 'address' => $address['address'], 'lng' => $address['lng'], 'lat' => $address['lat'], 'appoint_time' => $appointTime ?: null, 'base_amount' => $amount['base_amount'], 'floor_amount' => $amount['floor_amount'], 'urgent_amount' => $amount['urgent_amount'], 'discount_amount' => $amount['discount_amount'], 'total_amount' => $amount['total_amount'], 'pay_amount' => $amount['pay_amount'], 'status' => $priceType === 'quote' ? OrderStatus::QUOTING : OrderStatus::PENDING_PAY, 'pay_status' => 0, 'coupon_id' => $amount['coupon'] ? (int)$amount['coupon']['id'] : 0, 'verify_code' => $verifyCode, 'createtime' => time(), 'updatetime' => time(), ]); $modeText = [ 'fixed' => '一口价', 'quote' => '师傅报价', 'custom' => '用户自主定价', 'range' => '按面积计价', ]; if (!empty($goodsList) && is_array($goodsList)) { foreach ($goodsList as $g) { // 商品名称快照:优先读取自填名称 custom_name 或 item_name,确保非标自填不丢失 $finalGoodsName = ''; if (!empty($g['custom_name'])) { $finalGoodsName = trim((string)$g['custom_name']); } elseif (!empty($g['item_name'])) { $finalGoodsName = trim((string)$g['item_name']); } elseif (!empty($g['goods_name'])) { $finalGoodsName = trim((string)$g['goods_name']); } else { $finalGoodsName = (string)$serviceName; } // 规格型号快照:优先读取 spec_name 或 goods_model $finalSpecModel = ''; if (!empty($g['spec_name'])) { $finalSpecModel = trim((string)$g['spec_name']); } elseif (!empty($g['goods_model'])) { $finalSpecModel = trim((string)$g['goods_model']); } else { $finalSpecModel = (string)$goodsModel; } // 商品实物图/现场图 $finalImage = ''; if (!empty($g['goods_image'])) { $finalImage = (string)$g['goods_image']; } elseif (!empty($g['images']) && is_array($g['images'])) { $finalImage = (string)reset($g['images']); } // 增项服务快照 $addonStr = ''; if (!empty($g['addon_services'])) { $addonStr = is_array($g['addon_services']) ? json_encode($g['addon_services'], JSON_UNESCAPED_UNICODE) : (string)$g['addon_services']; } elseif (!empty($g['extras']) && is_array($g['extras'])) { $hitExtras = array_filter($g['extras'], function($ex) { return !empty($ex['num']) && (int)$ex['num'] > 0; }); if (!empty($hitExtras)) { $addonStr = json_encode(array_values($hitExtras), JSON_UNESCAPED_UNICODE); } } Db::name('order_goods')->insert([ 'order_id' => (int)$id, 'service_id' => isset($g['service_id']) ? (int)$g['service_id'] : (int)$serviceId, 'goods_name' => mb_substr($finalGoodsName, 0, 100), 'goods_model' => mb_substr($finalSpecModel, 0, 100), 'goods_num' => isset($g['num']) ? max(1, (int)$g['num']) : (isset($g['goods_num']) ? max(1, (int)$g['goods_num']) : 1), 'goods_image' => mb_substr($finalImage, 0, 255), 'addon_services' => mb_substr($addonStr, 0, 255), 'base_price' => isset($g['base_price']) ? (float)$g['base_price'] : (float)$amount['base_amount'], 'createtime' => time(), ]); } } $initStatus = $priceType === 'quote' ? OrderStatus::QUOTING : OrderStatus::PENDING_PAY; $this->addLog($id, $initStatus, '用户下单(' . (isset($modeText[$priceType]) ? $modeText[$priceType] : '一口价') . ')', 'user', $this->uid()); $this->success('下单成功', [ 'order_id' => (int)$id, 'order_no' => $orderNo, 'amount' => $amount, ]); } /** * 支付(联调模拟支付,pay_type=mock;上线换微信支付回调) * @ApiMethod (POST) */ public function pay() { $orderId = (int)$this->request->post('order_id'); $payType = $this->request->post('pay_type', 'mock'); $order = $this->ownOrder($orderId, 'user'); if ((int)$order['pay_status'] === 1) { $this->error('订单已支付,请勿重复支付'); } if ((int)$order['status'] !== OrderStatus::PENDING_PAY) { $this->error('订单状态不允许支付'); } Db::startTrans(); try { Db::name('order')->where('id', $orderId)->update([ 'pay_status' => 1, 'pay_type' => $payType, 'pay_time' => time(), 'transaction_id' => 'MOCK' . date('YmdHis') . mt_rand(1000, 9999), 'updatetime' => time(), ]); $nextStatus = $order['price_type'] === 'quote' ? OrderStatus::QUOTING : OrderStatus::PENDING_ACCEPT; $nextRemark = $nextStatus === OrderStatus::QUOTING ? '支付成功,订单已进入报价大厅,等待师傅报价' : '支付成功,订单已进入接单大厅'; $this->transit($order, $nextStatus, $nextRemark, [], 'user', $this->uid()); $this->addLog($orderId, $nextStatus, $nextStatus === OrderStatus::QUOTING ? '订单进入师傅报价大厅' : '订单进入师傅接单大厅', 'system', 0); // 核销优惠券 if ((int)$order['coupon_id'] > 0) { Db::name('user_coupon')->where('id', (int)$order['coupon_id'])->update([ 'status' => 'used', 'order_id' => $orderId, 'use_time' => time(), ]); } Db::commit(); } catch (\Exception $e) { Db::rollback(); $this->error('支付失败:' . $e->getMessage()); } // 订单广播通知:进入接单大厅后,通知周边在线师傅 if ($nextStatus === OrderStatus::PENDING_ACCEPT) { try { $masters = Db::name('master')->where('status', 'normal')->limit(20)->select(); foreach ($masters as $m) { $this->notify('master', (int)$m['user_id'], '新工单就绪通知', '同城有新工单【' . $order['order_no'] . '】进入接单大厅,请尽快前往抢单!', 'order', $orderId); } } catch (\Exception $e) {} } $this->success('支付成功'); } /** * 我的订单列表 */ /** * 用户在线支付施工增项补差价 * @ApiMethod (POST) */ public function payDiff() { $orderId = (int)$this->request->post('order_id'); $order = $this->ownOrder($orderId, 'user'); if ((int)$order['diff_status'] !== 1 || (float)$order['diff_amount'] <= 0) { $this->error('当前订单无待支付的增项补差价'); } $diffAmount = (float)$order['diff_amount']; $newPayAmount = round((float)$order['pay_amount'] + $diffAmount, 2); Db::startTrans(); try { Db::name('order')->where('id', $orderId)->update([ 'diff_status' => 2, 'pay_amount' => $newPayAmount, 'updatetime' => time(), ]); $this->addLog($orderId, (int)$order['status'], '用户在线支付施工增项补差价 ¥' . $diffAmount, 'user', $this->uid()); Db::commit(); } catch (\Exception $e) { Db::rollback(); $this->error('支付失败:' . $e->getMessage()); } if ((int)$order['master_id'] > 0) { $m = Db::name('master')->where('id', $order['master_id'])->find(); if ($m) { $this->notify('master', (int)$m['user_id'], '客户已支付增项补差价', '工单 ' . $order['order_no'] . ' 客户已支付增项补差价 ¥' . $diffAmount, 'order', $orderId); } } $this->success('增项补差价支付成功!', ['pay_amount' => $newPayAmount]); } public function index() { $status = $this->request->param('status', 'all'); $keyword = trim((string)$this->request->param('keyword', '')); $page = max(1, (int)$this->request->param('page', 1)); $limit = min(50, max(1, (int)$this->request->param('limit', 10))); $build = function () use ($status, $keyword) { $q = Db::name('order')->where('user_id', $this->uid())->where('user_deleted', 0); if ($keyword !== '') { $q->where(function ($sub) use ($keyword) { $sub->where('contact', 'like', "%{$keyword}%") ->whereOr('mobile', 'like', "%{$keyword}%") ->whereOr('order_no', 'like', "%{$keyword}%") ->whereOr('service_name', 'like', "%{$keyword}%") ->whereOr('address', 'like', "%{$keyword}%") ->whereOr('remark', 'like', "%{$keyword}%"); }); } if ($status === 'active') { $q->where('status', 'in', OrderStatus::activeValues()); } elseif ($status === 'completed' || $status === '8') { $q->where('status', OrderStatus::COMPLETED); } elseif ($status === 'cancelled' || $status === '9') { $q->where('status', OrderStatus::CANCELLED); } elseif ($status === 'pending_pay' || $status === '0') { $q->where('status', OrderStatus::PENDING_PAY); } elseif ($status === 'pending_accept' || $status === '1') { $q->where('status', OrderStatus::PENDING_ACCEPT); } elseif ($status === 'quoting' || $status === '2') { $q->where('status', OrderStatus::QUOTING); } elseif ($status === 'scheduled' || $status === '3') { $q->where('status', OrderStatus::ACCEPTED); } elseif ($status === 'serving' || $status === '4_6') { $q->where('status', 'in', [OrderStatus::SCHEDULED, OrderStatus::ARRIVED, OrderStatus::WORKING]); } elseif ($status === 'pending_verify' || $status === '7') { $q->where('status', OrderStatus::PENDING_VERIFY); } elseif ($status !== 'all' && $status !== '') { $q->where('status', (int)$status); } return $q; }; $total = $build()->count(); $list = $build()->order('id desc')->page($page, $limit)->select(); $orderIds = array_column($list, 'id'); $allGoods = []; if (!empty($orderIds)) { $goodsRows = Db::name('order_goods')->where('order_id', 'in', $orderIds)->select(); foreach ($goodsRows as $gr) { $allGoods[$gr['order_id']][] = $gr; } } foreach ($list as &$row) { $row = $this->formatOrder($row); if (!empty($allGoods[$row['id']])) { $row['goods_list'] = $allGoods[$row['id']]; $firstG = $allGoods[$row['id']][0]; $row['goods_title'] = $firstG['goods_name'] . (!empty($firstG['goods_model']) ? ' (' . $firstG['goods_model'] . ')' : '') . ' x ' . $firstG['goods_num'] . '套'; if (!empty($firstG['addon_services'])) { $row['service_items_text'] = '服务项目: ' . $firstG['addon_services']; } if (!empty($firstG['goods_image'])) { $row['goods_image_url'] = $firstG['goods_image']; } } } unset($row); $this->success('', ['list' => $list, 'total' => (int)$total, 'page' => $page]); } /** * 删除订单(用户端软删除,隐藏订单) * @ApiMethod (POST) */ public function delete() { $orderId = (int)$this->request->post('order_id'); $order = $this->ownOrder($orderId, 'user'); if (!in_array((int)$order['status'], [OrderStatus::CANCELLED, OrderStatus::COMPLETED], true)) { $this->error('仅已关闭或已完成的订单支持删除'); } Db::name('order')->where('id', $orderId)->update(['user_deleted' => 1, 'updatetime' => time()]); $this->success('订单已删除'); } /** * 订单详情(含状态轨迹与师傅信息) */ public function detail() { $orderId = (int)$this->request->param('id') ?: (int)$this->request->param('order_id'); if ($orderId <= 0) { $this->error('订单参数错误'); } $order = Db::name('order')->where('id', $orderId)->find(); if (!$order) { $this->error('订单不存在'); } // 本人订单,或同一商家(主账号 + 关联账号)的订单 if ((int)$order['user_id'] !== $this->uid()) { $uids = $this->currentMerchantUserIds(); if (!in_array((int)$order['user_id'], $uids, true)) { $this->error('无权操作该订单', null, 403); } } $logs = Db::name('order_log')->where('order_id', $orderId)->order('id asc')->select(); foreach ($logs as &$log) { $log['status_text'] = OrderStatus::label($log['status']); $log['time_text'] = date('m-d H:i', (int)$log['createtime']); } unset($log); $master = null; if ((int)$order['master_id'] > 0) { $m = Db::name('master')->where('id', $order['master_id'])->find(); if ($m) { $master = [ 'id' => (int)$m['id'], 'realname' => $m['realname'], 'mobile' => substr_replace($m['mobile'], '****', 3, 4), 'score' => (float)$m['score'], 'order_count' => (int)$m['order_count'], 'level' => (int)$m['level'], ]; } } $media = Db::name('order_media')->where('order_id', $orderId)->order('id asc')->select(); // 报价订单:返回师傅报价列表,供用户比价并选择师傅 $quotes = []; if ($order['price_type'] === 'quote') { $rows = Db::name('order_quote')->alias('q') ->join('master m', 'm.id = q.master_id', 'LEFT') ->field('q.id,q.order_id,q.master_id,q.amount,q.description,q.valid_hours,q.expire_time,q.status,q.createtime,m.realname,m.mobile,m.avatar,m.score,m.order_count,m.level') ->where('q.order_id', $orderId) ->order('q.amount asc,q.id asc') ->select(); $statusText = ['wait' => '等待选择', 'accept' => '已选中', 'reject' => '未选中', 'expired' => '已过期']; $now = time(); foreach ($rows as $q) { if ($q['status'] === 'wait' && (int)$q['expire_time'] > 0 && (int)$q['expire_time'] < $now) { Db::name('order_quote')->where('id', (int)$q['id'])->update(['status' => 'expired']); $q['status'] = 'expired'; } $quotes[] = [ 'id' => (int)$q['id'], 'master_id' => (int)$q['master_id'], 'realname' => $q['realname'] ?: '师傅', 'avatar' => $q['avatar'], 'score' => (float)$q['score'], 'order_count' => (int)$q['order_count'], 'level' => (int)$q['level'], 'amount' => (float)$q['amount'], 'description' => $q['description'], 'valid_hours' => (int)$q['valid_hours'], 'status' => $q['status'], 'status_text' => isset($statusText[$q['status']]) ? $statusText[$q['status']] : $q['status'], 'time_text' => date('m-d H:i', (int)$q['createtime']), 'expire_text' => (int)$q['expire_time'] > 0 ? date('m-d H:i', (int)$q['expire_time']) : '长期有效', ]; } } $creatorUser = Db::name('user')->where('id', (int)$order['user_id'])->find(); $orderContact = $creatorUser ? (($creatorUser['username'] ?: '用户') . ', ' . ($creatorUser['mobile'] ?: $order['mobile'])) : $order['mobile']; $formattedOrder = $this->formatOrder($order); $formattedOrder['order_contact'] = $orderContact; $formattedOrder['remark_display'] = !empty($order['remark']) ? $order['remark'] : '无备注'; $goodsList = Db::name('order_goods')->where('order_id', $orderId)->select(); if (empty($goodsList)) { $goodsList = [ [ 'goods_name' => $formattedOrder['goods_name'] ?: $formattedOrder['service_name'], 'goods_model' => $formattedOrder['goods_model'] ?: '', 'goods_num' => max(1, (int)$formattedOrder['goods_num']), 'goods_image' => $formattedOrder['goods_image_url'], 'addon_services' => '安装x' . max(1, (int)$formattedOrder['goods_num']) . ';', 'base_price' => (float)$formattedOrder['pay_amount'], ] ]; } $this->success('', [ 'order' => $formattedOrder, 'goods_list' => $goodsList, 'master' => $master, 'logs' => $logs, 'media' => $media, 'quotes' => $quotes, 'can_choose_quote' => $order['price_type'] === 'quote' && (int)$order['status'] === OrderStatus::QUOTING && (int)$order['master_id'] === 0, ]); } /** * 报价订单:用户选择师傅报价(选定后订单进入已接单) * @ApiMethod (POST) */ public function chooseQuote() { $orderId = (int)$this->request->post('order_id'); $quoteId = (int)$this->request->post('quote_id'); $order = $this->ownOrder($orderId, 'user'); if ($order['price_type'] !== 'quote') { $this->error('该订单不是师傅报价订单'); } if ((int)$order['status'] !== OrderStatus::QUOTING) { $this->error('当前状态(' . OrderStatus::label((int)$order['status']) . ')不可选择报价'); } if ((int)$order['master_id'] > 0) { $this->error('已选定师傅,请勿重复选择'); } $quote = Db::name('order_quote')->where('id', $quoteId)->where('order_id', $orderId)->find(); if (!$quote) { $this->error('报价不存在'); } if ($quote['status'] !== 'wait') { $this->error('该报价已失效,请选择其他师傅'); } if ((int)$quote['expire_time'] > 0 && (int)$quote['expire_time'] < time()) { Db::name('order_quote')->where('id', $quoteId)->update(['status' => 'expired']); $this->error('该报价已过期,请选择其他师傅'); } $master = Db::name('master')->where('id', (int)$quote['master_id'])->find(); if (!$master) { $this->error('师傅信息不存在'); } $quoteAmount = round((float)$quote['amount'], 2); $payAmount = max(0, round($quoteAmount - (float)$order['discount_amount'], 2)); $alreadyPaid = (int)$order['pay_status'] === 1 ? (float)$order['pay_amount'] : 0.0; $needDiff = max(0, round($payAmount - $alreadyPaid, 2)); $payType = $this->request->post('pay_type', 'wechat'); $remark = '客户已选择 ' . $master['realname'] . ' 的报价 ¥' . $quoteAmount; if ($needDiff > 0) { $remark .= '(补付差额 ¥' . $needDiff . ')'; } $remark .= ',等待师傅联系'; Db::startTrans(); try { $updateData = [ 'master_id' => (int)$quote['master_id'], 'accept_time' => time(), 'base_amount' => $quoteAmount, 'floor_amount' => 0, 'urgent_amount' => 0, 'total_amount' => $quoteAmount, 'pay_amount' => $payAmount, 'pay_status' => 1, ]; if ($needDiff > 0) { $updateData['pay_type'] = $payType; $updateData['pay_time'] = time(); } $this->transit($order, OrderStatus::ACCEPTED, $remark, $updateData, 'user', $this->uid()); Db::name('order_quote')->where('id', $quoteId)->update(['status' => 'accept']); Db::name('order_quote')->where('order_id', $orderId)->where('id', '<>', $quoteId) ->where('status', 'wait')->update(['status' => 'reject']); $this->notify('user', (int)$order['user_id'], '已选定服务师傅', $remark, 'order', $orderId); $this->notify('master', (int)$master['user_id'], '您的报价已被选中', '订单 ' . $order['order_no'] . ' 客户已选择您的报价 ¥' . $quoteAmount . ',请尽快联系客户', 'order', $orderId); Db::commit(); } catch (\Exception $e) { Db::rollback(); $this->error('选择报价失败:' . $e->getMessage()); } $this->success('已选定师傅并完成支付托管,请等待师傅联系', [ 'pay_amount' => $payAmount, 'need_diff' => $needDiff, 'already_pay' => $alreadyPaid, ]); } /** * 报价订单:报价列表(详情页轮询/刷新使用) */ public function quotes() { $orderId = (int)$this->request->param('order_id'); $order = $this->ownOrder($orderId, 'user'); if ($order['price_type'] !== 'quote') { $this->success('', ['list' => [], 'can_choose' => false]); } $rows = Db::name('order_quote')->alias('q') ->join('master m', 'm.id = q.master_id', 'LEFT') ->field('q.id,q.master_id,q.amount,q.description,q.valid_hours,q.expire_time,q.status,q.createtime,m.realname,m.avatar,m.score,m.order_count,m.level') ->where('q.order_id', $orderId) ->order('q.amount asc,q.id asc') ->select(); $statusText = ['wait' => '等待选择', 'accept' => '已选中', 'reject' => '未选中', 'expired' => '已过期']; $list = []; foreach ($rows as $q) { $list[] = [ 'id' => (int)$q['id'], 'master_id' => (int)$q['master_id'], 'realname' => $q['realname'] ?: '师傅', 'avatar' => $q['avatar'], 'score' => (float)$q['score'], 'order_count' => (int)$q['order_count'], 'amount' => (float)$q['amount'], 'description' => $q['description'], 'status' => $q['status'], 'status_text' => isset($statusText[$q['status']]) ? $statusText[$q['status']] : $q['status'], 'time_text' => date('m-d H:i', (int)$q['createtime']), 'expire_text' => (int)$q['expire_time'] > 0 ? date('m-d H:i', (int)$q['expire_time']) : '长期有效', ]; } $this->success('', [ 'list' => $list, 'can_choose' => (int)$order['status'] === OrderStatus::QUOTING && (int)$order['master_id'] === 0, ]); } /** * 取消订单(已支付则模拟退款) * @ApiMethod (POST) */ public function cancel() { $orderId = (int)$this->request->post('order_id'); $reason = trim((string)$this->request->post('reason', '用户取消')); $order = $this->ownOrder($orderId, 'user'); if (!OrderStatus::canCancelByUser($order['status'])) { $this->error('当前状态(' . OrderStatus::label($order['status']) . ')不可取消,请联系客服'); } Db::startTrans(); try { $extra = ['cancel_reason' => mb_substr($reason, 0, 255)]; if ((int)$order['pay_status'] === 1) { $extra['pay_status'] = 2; $extra['refund_amount'] = $order['pay_amount']; } $this->transit($order, OrderStatus::CANCELLED, '用户取消:' . $reason, $extra, 'user', $this->uid()); if ((int)$order['coupon_id'] > 0) { Db::name('user_coupon')->where('id', (int)$order['coupon_id'])->update([ 'status' => 'unused', 'order_id' => 0, 'use_time' => null, ]); } Db::commit(); } catch (\Exception $e) { Db::rollback(); $this->error('取消失败:' . $e->getMessage()); } $this->success('订单已取消'); } /** * 客户验收(验收码可选校验)→ 生成结算并给师傅入账 * @ApiMethod (POST) */ public function verify() { $orderId = (int)$this->request->post('order_id'); $code = trim((string)$this->request->post('verify_code', '')); $order = $this->ownOrder($orderId, 'user'); if ((int)$order['status'] !== OrderStatus::PENDING_VERIFY) { $this->error('当前状态(' . OrderStatus::label($order['status']) . ')不可验收'); } if ($code !== '' && $code !== (string)$order['verify_code']) { $this->error('验收码不正确'); } $masterId = (int)$order['master_id']; $payAmount = (float)$order['pay_amount']; $rate = 10.00; $commission = round($payAmount * $rate / 100, 2); $settleAmount = round($payAmount - $commission, 2); Db::startTrans(); try { $this->transit($order, OrderStatus::COMPLETED, '客户验收通过,订单完成', [ 'verify_time' => time(), 'settle_status' => 1, 'settle_amount' => $settleAmount, ], 'user', $this->uid()); Db::name('settle')->insert([ 'order_id' => $orderId, 'master_id' => $masterId, 'order_amount' => $payAmount, 'commission_rate' => $rate, 'commission' => $commission, 'settle_amount' => $settleAmount, 'status' => 'settled', 'settle_time' => time(), 'createtime' => time(), ]); if ($masterId > 0) { $master = Db::name('master')->where('id', $masterId)->lock(true)->find(); $newMoney = round((float)$master['money'] + $settleAmount, 2); Db::name('master')->where('id', $masterId)->update([ 'money' => $newMoney, 'order_count' => (int)$master['order_count'] + 1, 'updatetime' => time(), ]); Db::name('wallet_log')->insert([ 'user_type' => 'master', 'user_id' => $masterId, 'type' => 'settle', 'amount' => $settleAmount, 'balance' => $newMoney, 'order_id' => $orderId, 'remark' => '订单结算入账', 'createtime' => time(), ]); $this->notify('master', (int)$master['user_id'], '订单结算入账', '订单 ' . $order['order_no'] . ' 已验收,收入 ¥' . $settleAmount, 'order', $orderId); } Db::commit(); } catch (\Exception $e) { Db::rollback(); $this->error('验收失败:' . $e->getMessage()); } $this->success('验收成功,订单已完成'); } /** * 订单输出格式化 */ protected function formatOrder($row) { $status = (int)$row['status']; $appoint = (int)$row['appoint_time']; $mobile = (string)$row['mobile']; $mobileMask = strlen($mobile) === 11 ? substr_replace($mobile, '****', 3, 4) : $mobile; $prov = isset($row['province']) ? $row['province'] : ''; $city = isset($row['city']) ? $row['city'] : ''; $dist = isset($row['district']) ? $row['district'] : ''; $addr = isset($row['address']) ? $row['address'] : ''; $fullAddress = trim($prov . $city . $dist . $addr); if (empty($fullAddress)) { $fullAddress = $addr; } // 状态轨迹提示文案与时间 $statusTimelineText = OrderStatus::tip($status); $statusTime = !empty($row['updatetime']) ? (int)$row['updatetime'] : (int)$row['createtime']; if ($status === OrderStatus::CANCELLED) { $statusTimelineText = ((int)$row['pay_status'] === 0) ? '订单未支付,您已取消订单' : '订单已取消,退款处理中'; } elseif ($status === OrderStatus::PENDING_PAY) { $statusTimelineText = '订单待支付,请尽快完成支付'; } elseif ($status === OrderStatus::PENDING_ACCEPT) { $statusTimelineText = '订单已进入接单大厅,正在匹配师傅'; } elseif ($status === OrderStatus::QUOTING) { $statusTimelineText = '已有师傅进行报价,请比对选择'; } elseif ($status === OrderStatus::ACCEPTED) { $statusTimelineText = '师傅已接单,请等待师傅联系并上门'; if (!empty($row['accept_time'])) $statusTime = (int)$row['accept_time']; } elseif ($status === OrderStatus::SCHEDULED) { $statusTimelineText = '已预约上门服务时间:' . ($appoint ? date('Y-m-d H:i', $appoint) : '尽快上门'); } elseif ($status === OrderStatus::ARRIVED) { $statusTimelineText = '师傅已到达现场,准备开工'; if (!empty($row['arrive_time'])) $statusTime = (int)$row['arrive_time']; } elseif ($status === OrderStatus::WORKING) { $statusTimelineText = '师傅施工中,请稍候'; if (!empty($row['start_time'])) $statusTime = (int)$row['start_time']; } elseif ($status === OrderStatus::PENDING_VERIFY) { $statusTimelineText = '师傅已完工,请核验施工质量后验收'; if (!empty($row['finish_time'])) $statusTime = (int)$row['finish_time']; } elseif ($status === OrderStatus::COMPLETED) { $statusTimelineText = '服务已完成,期待您的评价'; if (!empty($row['verify_time'])) $statusTime = (int)$row['verify_time']; } // 期望上门文本 $appointDateText = '尽快上门'; if ($appoint > 0) { $hour = (int)date('H', $appoint); $period = $hour < 12 ? '上午' : ($hour < 18 ? '下午' : '晚上'); $appointDateText = date('Y-n-j', $appoint) . ' ' . $period; } // 商品与服务概览 $goodsNum = max(1, (int)$row['goods_num']); $goodsTitle = ($row['service_name'] ?: '专业师傅上门服务') . ' x ' . $goodsNum . '套'; $serviceItemsText = '服务项目: 安装x' . $goodsNum . ';'; if (!empty($row['goods_model'])) { $serviceItemsText = '规格型号: ' . $row['goods_model'] . '; 安装x' . $goodsNum . ';'; } // 默认商品图/服务图 $goodsImageUrl = !empty($row['service_image']) ? $row['service_image'] : '/static/icons/appliance.png'; return array_merge($row, [ 'id' => (int)$row['id'], 'status' => $status, 'status_text' => OrderStatus::label($status), 'status_tip' => OrderStatus::tip($status), 'status_timeline_text'=> $statusTimelineText, 'status_time_text' => date('Y-m-d H:i:s', $statusTime), 'status_time_short' => date('Y/m/d H:i:s', $statusTime), 'can_cancel' => OrderStatus::canCancelByUser($status), 'can_delete' => in_array($status, [OrderStatus::CANCELLED, OrderStatus::COMPLETED], true), 'can_verify' => $status === OrderStatus::PENDING_VERIFY, 'can_review' => $status === OrderStatus::COMPLETED && (int)$row['is_review'] === 0, 'can_pay' => $status === OrderStatus::PENDING_PAY, 'pay_amount' => (float)$row['pay_amount'], 'total_amount' => (float)$row['total_amount'], 'appoint_text' => $appoint ? date('m-d H:i', $appoint) : '尽快上门', 'appoint_date_text' => $appointDateText, 'create_text' => date('Y-m-d H:i', (int)$row['createtime']), 'create_full_text' => date('Y/m/d H:i:s', (int)$row['createtime']), 'urgent' => (int)$row['urgent'], 'price_type_text' => $this->priceTypeText($row['price_type']), 'price_type_tag' => $row['price_type'] === 'quote' ? '报价' : ($row['price_type'] === 'custom' ? '自定价' : '一口价'), 'is_order_owner' => (int)$row['user_id'] === $this->uid(), 'mobile_mask' => $mobileMask, 'full_address' => $fullAddress, 'goods_title' => $goodsTitle, 'service_items_text' => $serviceItemsText, 'goods_image_url' => $goodsImageUrl, 'diff_amount' => isset($row['diff_amount']) ? (float)$row['diff_amount'] : 0.00, 'diff_status' => isset($row['diff_status']) ? (int)$row['diff_status'] : 0, 'diff_remark' => isset($row['diff_remark']) ? (string)$row['diff_remark'] : '', 'diff_image' => isset($row['diff_image']) ? (string)$row['diff_image'] : '', 'empty_run_fee' => isset($row['empty_run_fee']) ? (float)$row['empty_run_fee'] : 0.00, 'auto_quote_timeout' => isset($row['auto_quote_timeout']) ? (int)$row['auto_quote_timeout'] : 0, ]); } /** * 当前用户所属商家的全部账号(主账号 + 关联账号) */ protected function currentMerchantUserIds() { $merchant = Db::name('merchant')->where('user_id', $this->uid())->find(); if (!$merchant) { $bind = Db::name('merchant_member')->where('user_id', $this->uid())->find(); if ($bind) { $merchant = Db::name('merchant')->where('id', (int)$bind['merchant_id'])->find(); } } if (!$merchant) { return []; } $ids = [(int)$merchant['user_id']]; $rows = Db::name('merchant_member')->where('merchant_id', (int)$merchant['id'])->column('user_id'); foreach ($rows as $id) { $id = (int)$id; if ($id > 0 && !in_array($id, $ids, true)) { $ids[] = $id; } } return $ids; } /** * 计价方式文案(前后端与后台统一口径) */ protected function priceTypeText($type) { $map = [ 'fixed' => '一口价', 'quote' => '师傅报价', 'custom' => '用户自主定价', 'range' => '按面积计价', ]; return isset($map[$type]) ? $map[$type] : '一口价'; } /** * 超时未验收订单自动核销(默认48小时无争议自动验收结算,保障师傅权益) */ public static function autoVerifyExpired($timeoutSeconds = 172800) { $expiredTime = time() - (int)$timeoutSeconds; $rows = Db::name('order') ->where('status', OrderStatus::PENDING_VERIFY) ->where('settle_status', 0) ->where('finish_time', '<', $expiredTime) ->limit(20) ->select(); $count = 0; foreach ($rows as $order) { $orderId = (int)$order['id']; $masterId = (int)$order['master_id']; $payAmount = (float)$order['pay_amount']; $rate = 10.00; $commission = round($payAmount * $rate / 100, 2); $settleAmount = round($payAmount - $commission, 2); Db::startTrans(); try { Db::name('order')->where('id', $orderId)->update([ 'status' => OrderStatus::COMPLETED, 'verify_time' => time(), 'settle_status' => 1, 'settle_amount' => $settleAmount, 'updatetime' => time(), ]); Db::name('order_log')->insert([ 'order_id' => $orderId, 'status' => OrderStatus::COMPLETED, 'remark' => '完工超48小时未验收,系统自动确认完成并结算', 'operator_type' => 'system', 'operator_id' => 0, 'createtime' => time(), ]); Db::name('settle')->insert([ 'order_id' => $orderId, 'master_id' => $masterId, 'order_amount' => $payAmount, 'commission_rate' => $rate, 'commission' => $commission, 'settle_amount' => $settleAmount, 'status' => 'settled', 'settle_time' => time(), 'createtime' => time(), ]); if ($masterId > 0) { $master = Db::name('master')->where('id', $masterId)->lock(true)->find(); if ($master) { $newMoney = round((float)$master['money'] + $settleAmount, 2); Db::name('master')->where('id', $masterId)->update([ 'money' => $newMoney, 'order_count' => (int)$master['order_count'] + 1, 'updatetime' => time(), ]); Db::name('wallet_log')->insert([ 'user_type' => 'master', 'user_id' => $masterId, 'type' => 'settle', 'amount' => $settleAmount, 'balance' => $newMoney, 'order_id' => $orderId, 'remark' => '工单超时自动验收结算入账', 'createtime' => time(), ]); } } Db::commit(); $count++; } catch (\Exception $e) { Db::rollback(); } } return $count; } }