success('', ['list' => OrderStatus::all()]); } /** * 下单(创建待支付订单) * @ApiMethod (POST) */ public function create() { $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', ''); $service = Db::name('service')->where('id', $serviceId)->where('status', 'normal')->find(); if (!$service) { $this->error('服务不存在或已下架'); } // 下单页可选择:一口价 / 师傅报价 / 用户自主定价(未传则沿用服务默认) $priceType = trim((string)$this->request->post('price_type', $service['price_type'])); if (!in_array($priceType, ['fixed', 'quote', 'custom', 'range'], true)) { $priceType = $service['price_type']; } $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 = Db::name('address')->where('id', $addressId)->where('user_id', $this->uid())->find(); if (!$address) { $this->error('请选择服务地址'); } if ($appointTime > 0 && $appointTime < time()) { $this->error('预约时间不能早于当前时间'); } // 金额统一由 Home::calcAmount 计算,保证与试算口径一致 $amount = Home::calcAmount($service, [ 'floor' => (int)$address['floor'], 'has_elevator' => (int)$address['has_elevator'], 'urgent' => $urgent, ], $couponId, $this->uid(), $priceType, $userPrice); $amount['price_type'] = $priceType; $orderNo = $this->makeOrderNo(); $verifyCode = (string)mt_rand(1000, 9999); $id = Db::name('order')->insertGetId([ 'order_no' => $orderNo, 'user_id' => $this->uid(), 'master_id' => 0, 'cat_id' => (int)$service['cat_id'], 'service_id' => (int)$service['id'], 'service_name' => $service['name'], 'service_image' => $service['image'], 'price_type' => $priceType, 'goods_images' => is_array($goodsImages) ? json_encode($goodsImages, JSON_UNESCAPED_UNICODE) : (string)$goodsImages, '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' => 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' => '按面积计价', ]; $this->addLog($id, OrderStatus::PENDING_PAY, '用户下单(' . (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()); } $this->success('支付成功'); } /** * 我的订单列表 */ public function index() { $status = $this->request->param('status', 'all'); $page = max(1, (int)$this->request->param('page', 1)); $limit = min(50, max(1, (int)$this->request->param('limit', 10))); // 注意:TP5 中 count() 执行后查询条件会被重置,必须重新构建查询 $build = function () use ($status) { $q = Db::name('order')->where('user_id', $this->uid()); if ($status === 'active') { $q->where('status', 'in', OrderStatus::activeValues()); } elseif ($status === 'completed') { $q->where('status', OrderStatus::COMPLETED); } elseif ($status === 'cancelled') { $q->where('status', OrderStatus::CANCELLED); } elseif ($status !== 'all' && $status !== '') { $q->where('status', (int)$status); } return $q; }; $total = $build()->count(); $list = $build()->order('id desc')->page($page, $limit)->select(); foreach ($list as &$row) { $row = $this->formatOrder($row); } unset($row); $this->success('', ['list' => $list, 'total' => (int)$total, 'page' => $page]); } /** * 订单详情(含状态轨迹与师傅信息) */ public function detail() { $orderId = (int)$this->request->param('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']) : '长期有效', ]; } } $this->success('', [ 'order' => $this->formatOrder($order), '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)); $remark = '客户已选择 ' . $master['realname'] . ' 的报价 ¥' . $quoteAmount . ',等待师傅联系'; Db::startTrans(); try { $this->transit($order, OrderStatus::ACCEPTED, $remark, [ 'master_id' => (int)$quote['master_id'], 'accept_time' => time(), 'base_amount' => $quoteAmount, 'floor_amount' => 0, 'urgent_amount' => 0, 'total_amount' => $quoteAmount, 'pay_amount' => $payAmount, ], '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('已选定师傅,请等待师傅联系'); } /** * 报价订单:报价列表(详情页轮询/刷新使用) */ 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']; return array_merge($row, [ 'id' => (int)$row['id'], 'status' => $status, 'status_text' => OrderStatus::label($status), 'status_tip' => OrderStatus::tip($status), 'can_cancel' => OrderStatus::canCancelByUser($status), '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) : '尽快上门', 'create_text' => date('Y-m-d H:i', (int)$row['createtime']), 'urgent' => (int)$row['urgent'], 'price_type_text' => $this->priceTypeText($row['price_type']), 'is_order_owner' => (int)$row['user_id'] === $this->uid(), ]); } /** * 当前用户所属商家的全部账号(主账号 + 关联账号) */ 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] : '一口价'; } }