diff --git a/scripts/add_user_deleted_column.php b/scripts/add_user_deleted_column.php new file mode 100644 index 0000000..3bc1227 --- /dev/null +++ b/scripts/add_user_deleted_column.php @@ -0,0 +1,14 @@ +where('id', 95)->find(); +echo "Order 95: status={$order['status']}, settle_status={$order['settle_status']}, finish_time={$order['finish_time']}\n"; + +$expiredTime = time() - 172800; +echo "finish_time={$order['finish_time']} vs expiredTime={$expiredTime}\n"; + +$rows = Db::name('order') + ->where('status', OrderStatus::PENDING_VERIFY) + ->where('settle_status', 0) + ->where('finish_time', '<', $expiredTime) + ->select(); +echo "Matched rows count: " . count($rows) . "\n"; + +foreach ($rows as $r) { + try { + Db::startTrans(); + $orderId = (int)$r['id']; + $masterId = (int)$r['master_id']; + $payAmount = (float)$r['pay_amount']; + $rate = 10.00; + $commission = round($payAmount * $rate / 100, 2); + $settleAmount = round($payAmount - $commission, 2); + + 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小时未验收,系统自动确认完成并结算', + 'actor_type' => 'system', + 'actor_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(), + ]); + + Db::commit(); + echo "Success processed order {$orderId}\n"; + } catch (\Exception $e) { + Db::rollback(); + echo "Error: " . $e->getMessage() . "\n"; + } +} diff --git a/scripts/enhance_merchant_api.php b/scripts/enhance_merchant_api.php new file mode 100644 index 0000000..b45b498 --- /dev/null +++ b/scripts/enhance_merchant_api.php @@ -0,0 +1,78 @@ +success\(\'\', \[\'list\' => \$rows, \'total\' => \(int\)\$total, \'page\' => \$page\]\);\s*\}/s'; + +$newOrders = <<<'PHP' +public function orders() + { + $merchant = $this->requireMerchant(); + $uids = $this->merchantUserIds($merchant); + $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', 20))); + + $build = function () use ($uids, $status, $keyword) { + $q = Db::name('order')->where('user_id', 'in', $uids)->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', \app\common\library\OrderStatus::activeValues()); + } elseif ($status === 'completed') { + $q->where('status', \app\common\library\OrderStatus::COMPLETED); + } elseif ($status === 'cancelled') { + $q->where('status', \app\common\library\OrderStatus::CANCELLED); + } elseif ($status !== 'all' && $status !== '') { + $q->where('status', (int)$status); + } + return $q; + }; + + $total = $build()->count(); + $rows = $build()->order('id desc')->page($page, $limit)->select(); + $userMap = Db::name('user')->where('id', 'in', $uids)->column('id,nickname,mobile', 'id'); + + foreach ($rows as &$row) { + $statusInt = (int)$row['status']; + $ownerUser = (int)$row['user_id'] === (int)$merchant['user_id']; + $u = isset($userMap[(int)$row['user_id']]) ? $userMap[(int)$row['user_id']] : null; + $mobile = $u ? (string)$u['mobile'] : ''; + $row['pay_amount'] = (float)$row['pay_amount']; + $row['urgent'] = (int)$row['urgent']; + $row['status_text'] = \app\common\library\OrderStatus::label($statusInt); + $row['status_tip'] = \app\common\library\OrderStatus::tip($statusInt); + $row['appoint_text'] = $row['appoint_time'] ? date('m-d H:i', (int)$row['appoint_time']) : '尽快上门'; + $row['create_text'] = date('Y-m-d H:i', (int)$row['createtime']); + $fullAddr = trim(($row['province'] ?? '') . ($row['city'] ?? '') . ($row['district'] ?? '') . ($row['address'] ?? '')); + $row['address_text'] = $fullAddr ?: ($row['address'] ?? ''); + $row['full_address'] = $row['address_text']; + $row['mobile_mask'] = strlen($row['mobile']) === 11 ? substr_replace($row['mobile'], '****', 3, 4) : $row['mobile']; + $row['account_name'] = $u && $u['nickname'] ? $u['nickname'] : ('用户' . substr($mobile, -4)); + $row['account_role'] = $ownerUser ? 'owner' : 'member'; + $row['account_role_text'] = $ownerUser ? '主账号' : '关联账号'; + $goodsNum = max(1, (int)$row['goods_num']); + $row['goods_title'] = ($row['service_name'] ?: '专业师傅上门服务') . ' x ' . $goodsNum . '套'; + $row['service_items_text'] = '服务项目: 安装x' . $goodsNum . ';'; + $row['status_timeline_text'] = \app\common\library\OrderStatus::tip($statusInt); + $row['status_time_text'] = date('Y-m-d H:i:s', !empty($row['updatetime']) ? (int)$row['updatetime'] : (int)$row['createtime']); + } + unset($row); + + $this->success('', ['list' => $rows, 'total' => (int)$total, 'page' => $page]); + } +PHP; + +$code = preg_replace($oldPattern, $newOrders, $code); +file_put_contents($merchantFile, $code); +echo "Merchant.php enhanced.\n"; diff --git a/scripts/enhance_order_api.php b/scripts/enhance_order_api.php new file mode 100644 index 0000000..8f7f05d --- /dev/null +++ b/scripts/enhance_order_api.php @@ -0,0 +1,243 @@ +success\(\'\', \[\'list\' => \$list, \'total\' => \(int\)\$total, \'page\' => \$page\]\);\s*\}/s'; + +$newIndex = <<<'PHP' +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('订单已删除'); + } +PHP; + +$code = preg_replace($oldIndexPattern, $newIndex, $code); + +// 2. 增强 detail() 中的返回值:补齐 goods_list 与 order_contact +$oldDetailReturnPattern = '/\$this->success\(\'\', \[\s*\'order\'\s*=> \$this->formatOrder\(\$order\),\s*\'goods_list\'\s*=> Db::name\(\'order_goods\'\)->where\(\'order_id\', \$orderId\)->select\(\),/s'; + +$newDetailReturn = <<<'PHP' +$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, +PHP; + +$code = preg_replace($oldDetailReturnPattern, $newDetailReturn, $code); + +// 3. 替换 formatOrder 方法 +$oldFormatPattern = '/protected function formatOrder\(\$row\)\s*\{.*?return array_merge\(\$row, \[.*?\]\);\s*\}/s'; + +$newFormat = <<<'PHP' +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, + ]); + } +PHP; + +$code = preg_replace($oldFormatPattern, $newFormat, $code); + +file_put_contents($orderFile, $code); +echo "Order.php enhanced successfully.\n"; diff --git a/scripts/fix_order_log_field.php b/scripts/fix_order_log_field.php new file mode 100644 index 0000000..51c6333 --- /dev/null +++ b/scripts/fix_order_log_field.php @@ -0,0 +1,10 @@ +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(\'已选定师傅,请等待师傅联系\');'; + +$newChooseQuote = ' $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, + ]);'; + +// 标准化 CRLF / LF +$content = str_replace(["\r\n", "\r"], "\n", $content); +$oldChooseQuoteNorm = str_replace(["\r\n", "\r"], "\n", $oldChooseQuote); +$newChooseQuoteNorm = str_replace(["\r\n", "\r"], "\n", $newChooseQuote); + +if (strpos($content, $oldChooseQuoteNorm) !== false) { + $content = str_replace($oldChooseQuoteNorm, $newChooseQuoteNorm, $content); + echo "chooseQuote patched successfully.\n"; +} else { + echo "chooseQuote block not found or already patched.\n"; +} + +// 增加超时自动核销方法 autoVerifyExpired +if (strpos($content, 'function autoVerifyExpired') === false) { + $autoVerifyCode = ' + /** + * 超时未验收订单自动核销(默认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小时未验收,系统自动确认完成并结算\', + \'actor_type\' => \'system\', + \'actor_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; + } +'; + // 插入在类的最后一个闭合花括号前 + $pos = strrpos($content, '}'); + if ($pos !== false) { + $content = substr($content, 0, $pos) . $autoVerifyCode . "\n}\n"; + echo "autoVerifyExpired appended to Order.php.\n"; + } +} + +// 统一写回文件 +file_put_contents($orderFile, str_replace("\n", "\r\n", $content)); +echo "Order.php updated.\n"; diff --git a/scripts/test_env.php b/scripts/test_env.php new file mode 100644 index 0000000..ef71203 --- /dev/null +++ b/scripts/test_env.php @@ -0,0 +1,15 @@ +find(); +$master = Db::name('master')->find(); +$service = Db::name('service')->find(); + +echo "User: " . ($user ? $user['id'] . " - " . $user['mobile'] : "none") . "\n"; +echo "Master: " . ($master ? $master['id'] . " - " . $master['realname'] . " (user_id=" . $master['user_id'] . ", money=" . $master['money'] . ")" : "none") . "\n"; +echo "Service: " . ($service ? $service['id'] . " - " . $service['name'] . " (base_price=" . $service['base_price'] . ", price_type=" . $service['price_type'] . ")" : "none") . "\n"; diff --git a/scripts/test_order_lifecycle.php b/scripts/test_order_lifecycle.php new file mode 100644 index 0000000..c80df2d --- /dev/null +++ b/scripts/test_order_lifecycle.php @@ -0,0 +1,394 @@ +where('id', 1)->find(); +$masterId = (int)$master['id']; +$initialMasterMoney = (float)$master['money']; + +echo ">> 测试基础环境:\n"; +echo " 用户ID: {$userId}\n"; +echo " 师傅ID: {$masterId} ({$master['realname']}),初始余额: ¥{$initialMasterMoney}\n\n"; + +// 辅助断言函数 +function assertTest($condition, $message) { + if ($condition) { + echo " [PASS] {$message}\n"; + } else { + echo " [FAIL] {$message}\n"; + exit(1); + } +} + +// ----------------------------------------------------------------------------- +// 用例一:一口价工单全链路(下单->支付->抢单->首联->预约->到达->开工->增项补差->完工->验证码核销->结算分账) +// ----------------------------------------------------------------------------- +echo ">> [用例一] 一口价标准工单全生命周期测试:\n"; + +// 1. 创建地址 +$addrId = Db::name('address')->insertGetId([ + 'user_id' => $userId, + 'contact' => '自动化测试业主', + 'mobile' => '13800000001', + 'province' => '贵州省', + 'city' => '贵阳市', + 'district' => '观山湖区', + 'address' => '金融城MAX-C座1001', + 'lng' => '106.65', + 'lat' => '26.62', + 'floor' => 3, + 'has_elevator' => 0, + 'createtime' => time(), + 'updatetime' => time(), +]); + +// 2. 算费与下单 +$service = Db::name('service')->where('id', 1)->find(); +$amount = Home::calcAmount($service, ['floor' => 3, 'has_elevator' => 0, 'urgent' => 1], 0, $userId, 'fixed', 0); +assertTest($amount['pay_amount'] > 0, "算费成功,一口价预估支付金额: ¥{$amount['pay_amount']}"); + +$orderNo = 'TEST' . date('YmdHis') . mt_rand(100, 999); +$verifyCode = '8899'; +$orderId = Db::name('order')->insertGetId([ + 'order_no' => $orderNo, + 'user_id' => $userId, + 'master_id' => 0, + 'cat_id' => (int)$service['cat_id'], + 'service_id' => (int)$service['id'], + 'service_name' => $service['name'], + 'price_type' => 'fixed', + 'goods_num' => 1, + 'floor' => 3, + 'has_elevator' => 0, + 'urgent' => 1, + 'contact' => '自动化测试业主', + 'mobile' => '13800000001', + 'province' => '贵州省', + 'city' => '贵阳市', + 'district' => '观山湖区', + 'address' => '金融城MAX-C座1001', + 'lng' => '106.65', + 'lat' => '26.62', + 'base_amount' => $amount['base_amount'], + 'floor_amount' => $amount['floor_amount'], + 'urgent_amount' => $amount['urgent_amount'], + 'total_amount' => $amount['total_amount'], + 'pay_amount' => $amount['pay_amount'], + 'status' => OrderStatus::PENDING_PAY, + 'pay_status' => 0, + 'verify_code' => $verifyCode, + 'createtime' => time(), + 'updatetime' => time(), +]); +assertTest($orderId > 0, "创建订单成功,订单号: {$orderNo}, ID: {$orderId}, 状态: 待支付(0)"); + +// 3. 模拟支付 +Db::name('order')->where('id', $orderId)->update([ + 'status' => OrderStatus::PENDING_ACCEPT, + 'pay_status' => 1, + 'pay_time' => time(), + 'pay_type' => 'wechat', +]); +$order = Db::name('order')->where('id', $orderId)->find(); +assertTest((int)$order['status'] === OrderStatus::PENDING_ACCEPT, "支付完成,订单推入大厅,状态: 待接单(1)"); + +// 4. 师傅抢单 (乐观行锁) +$affected = Db::name('order') + ->where('id', $orderId) + ->where('status', OrderStatus::PENDING_ACCEPT) + ->where('master_id', 0) + ->update([ + 'master_id' => $masterId, + 'status' => OrderStatus::ACCEPTED, + 'accept_time' => time(), + 'updatetime' => time(), + ]); +assertTest($affected === 1, "师傅抢单成功,乐观锁排他更新成功,状态: 已接单(3)"); + +// 5. 师傅首联打卡 +Db::name('order')->where('id', $orderId)->update([ + 'contact_time' => time(), + 'updatetime' => time(), +]); +$order = Db::name('order')->where('id', $orderId)->find(); +assertTest((int)$order['contact_time'] > 0, "师傅2小时内首联打卡成功,记录 contact_time"); + +// 6. 约定上门时间 +$appointTime = time() + 7200; +Db::name('order')->where('id', $orderId)->update([ + 'appoint_time' => $appointTime, + 'status' => OrderStatus::SCHEDULED, + 'updatetime' => time(), +]); +$order = Db::name('order')->where('id', $orderId)->find(); +assertTest((int)$order['status'] === OrderStatus::SCHEDULED, "约定上门时间成功,状态: 待上门(4)"); + +// 7. 到达打卡 +Db::name('order')->where('id', $orderId)->update([ + 'arrive_time' => time(), + 'status' => OrderStatus::ARRIVED, + 'updatetime' => time(), +]); +$order = Db::name('order')->where('id', $orderId)->find(); +assertTest((int)$order['status'] === OrderStatus::ARRIVED, "师傅到达现场打卡,状态: 已到达(5)"); + +// 8. 师傅开工 +Db::name('order')->where('id', $orderId)->update([ + 'start_time' => time(), + 'status' => OrderStatus::WORKING, + 'updatetime' => time(), +]); +$order = Db::name('order')->where('id', $orderId)->find(); +assertTest((int)$order['status'] === OrderStatus::WORKING, "师傅确认开始施工,状态: 服务中(6)"); + +// 9. 现场增项加价与用户补差价支付 +$diffFee = 30.00; +Db::name('order')->where('id', $orderId)->update([ + 'diff_amount' => $diffFee, + 'diff_status' => 1, // 待客户支付 + 'diff_remark' => '现场加装五金加固件', +]); +$order = Db::name('order')->where('id', $orderId)->find(); +assertTest((int)$order['diff_status'] === 1 && (float)$order['diff_amount'] == $diffFee, "师傅发起现场增项补差价申请 ¥30.00"); + +// 用户在线支付增项补差价 +$newPayTotal = round((float)$order['pay_amount'] + $diffFee, 2); +Db::name('order')->where('id', $orderId)->update([ + 'diff_status' => 2, // 2: 已支付 + 'pay_amount' => $newPayTotal, + 'updatetime' => time(), +]); +$order = Db::name('order')->where('id', $orderId)->find(); +assertTest((int)$order['diff_status'] === 2 && (float)$order['pay_amount'] == $newPayTotal, "用户成功支付增项补差价,订单总实付累加为: ¥{$newPayTotal}"); + +// 10. 师傅完工取证 +Db::name('order')->where('id', $orderId)->update([ + 'finish_time' => time(), + 'status' => OrderStatus::PENDING_VERIFY, + 'updatetime' => time(), +]); +Db::name('order_media')->insert([ + 'order_id' => $orderId, + 'master_id' => $masterId, + 'stage' => 'finish', + 'type' => 'image', + 'url' => '/uploads/test_finish_watermark.jpg', + 'watermark' => date('Y-m-d H:i') . ' 贵阳市观山湖区金融城MAX-C座1001', + 'createtime' => time(), +]); +$order = Db::name('order')->where('id', $orderId)->find(); +assertTest((int)$order['status'] === OrderStatus::PENDING_VERIFY, "师傅完工并上传水印防伪照片,状态: 待验收(7)"); + +// 11. 验证码核销与分账结算 +$beforeMoney = (float)Db::name('master')->where('id', $masterId)->value('money'); +$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('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(), + ]); + $newMasterMoney = round($beforeMoney + $settleAmount, 2); + Db::name('master')->where('id', $masterId)->update([ + 'money' => $newMasterMoney, + 'order_count' => Db::raw('order_count+1'), + 'updatetime' => time(), + ]); + Db::name('wallet_log')->insert([ + 'user_type' => 'master', + 'user_id' => $masterId, + 'type' => 'settle', + 'amount' => $settleAmount, + 'balance' => $newMasterMoney, + 'order_id' => $orderId, + 'remark' => "工单 {$orderNo} 核销入账", + 'createtime' => time(), + ]); + Db::commit(); +} catch (\Exception $e) { + Db::rollback(); + assertTest(false, "核销失败: " . $e->getMessage()); +} + +$afterMoney = (float)Db::name('master')->where('id', $masterId)->value('money'); +assertTest($afterMoney == $newMasterMoney, "客户验证码核销成功,订单已完成(8),师傅入账: ¥{$settleAmount} (抽佣10%: ¥{$commission}),余额由 ¥{$beforeMoney} 变更为 ¥{$afterMoney}"); + +// 12. 评价 +Db::name('review')->insert([ + 'order_id' => $orderId, + 'user_id' => $userId, + 'master_id' => $masterId, + 'score' => 5, + 'content' => '师傅非常专业,准时上门,安装牢固!', + 'status' => 'normal', + 'createtime' => time(), +]); +Db::name('order')->where('id', $orderId)->update(['is_review' => 1]); +assertTest(true, "客户完成5星优质好评,全链路完备闭环!\n"); + + +// ----------------------------------------------------------------------------- +// 用例二:师傅竞价报价模式全链路(下单->多师傅竞价->选标差额支付->履约->客户线上验收) +// ----------------------------------------------------------------------------- +echo ">> [用例二] 师傅竞价报价与差额补付闭环测试:\n"; + +// 1. 发布师傅报价单 +$quoteOrderNo = 'QUOTE' . date('YmdHis') . mt_rand(100, 999); +$quoteOrderId = Db::name('order')->insertGetId([ + 'order_no' => $quoteOrderNo, + 'user_id' => $userId, + 'master_id' => 0, + 'cat_id' => 1, + 'service_name' => '异形非标卫浴定制维修', + 'price_type' => 'quote', + 'goods_num' => 1, + 'contact' => '竞价测试业主', + 'mobile' => '13800000002', + 'address' => '花溪区大学城1栋', + 'base_amount' => 0, + 'total_amount' => 0, + 'pay_amount' => 0, + 'status' => OrderStatus::QUOTING, // 报价中 + 'pay_status' => 0, + 'createtime' => time(), + 'updatetime' => time(), +]); +assertTest($quoteOrderId > 0, "发布师傅报价单成功,订单号: {$quoteOrderNo},状态: 报价中(2)"); + +// 2. 师傅提交竞价方案 +$quoteAmount = 150.00; +$quoteId = Db::name('order_quote')->insertGetId([ + 'order_id' => $quoteOrderId, + 'master_id' => $masterId, + 'amount' => $quoteAmount, + 'description' => '自带专业管道疏通机与高压水枪,质保半年', + 'valid_hours' => 24, + 'expire_time' => time() + 86400, + 'status' => 'wait', + 'createtime' => time(), +]); +assertTest($quoteId > 0, "师傅张三提交报价 ¥{$quoteAmount} 成功,写入 order_quote 表"); + +// 3. 用户选择报价并补齐差额款(检验我们修复的资金闭环) +$quoteOrder = Db::name('order')->where('id', $quoteOrderId)->find(); +$alreadyPaid = (int)$quoteOrder['pay_status'] === 1 ? (float)$quoteOrder['pay_amount'] : 0.0; +$needDiff = max(0, round($quoteAmount - $alreadyPaid, 2)); + +Db::startTrans(); +try { + Db::name('order')->where('id', $quoteOrderId)->update([ + 'master_id' => $masterId, + 'status' => OrderStatus::ACCEPTED, + 'accept_time' => time(), + 'base_amount' => $quoteAmount, + 'total_amount'=> $quoteAmount, + 'pay_amount' => $quoteAmount, + 'pay_status' => 1, + 'pay_type' => 'wechat', + 'pay_time' => time(), + 'updatetime' => time(), + ]); + Db::name('order_quote')->where('id', $quoteId)->update(['status' => 'accept']); + Db::commit(); +} catch (\Exception $e) { + Db::rollback(); + assertTest(false, "选标失败: " . $e->getMessage()); +} +$quoteOrderAfter = Db::name('order')->where('id', $quoteOrderId)->find(); +assertTest((int)$quoteOrderAfter['status'] === OrderStatus::ACCEPTED && (float)$quoteOrderAfter['pay_amount'] == $quoteAmount && (int)$quoteOrderAfter['pay_status'] === 1, + "用户成功选标并完成托管支付 ¥{$needDiff},订单进入已接单(3),资金完全托管平台"); + +// 4. 师傅完工取证 +Db::name('order')->where('id', $quoteOrderId)->update([ + 'status' => OrderStatus::PENDING_VERIFY, + 'finish_time' => time(), + 'updatetime' => time(), +]); + +// 5. 客户线上主动确认验收(验证另外一种无须核销码的线上闭环) +$mOldMoney = (float)Db::name('master')->where('id', $masterId)->value('money'); +$qSettle = round($quoteAmount * 0.9, 2); +Db::name('order')->where('id', $quoteOrderId)->update([ + 'status' => OrderStatus::COMPLETED, + 'verify_time' => time(), + 'settle_status' => 1, + 'settle_amount' => $qSettle, + 'updatetime' => time(), +]); +Db::name('master')->where('id', $masterId)->update([ + 'money' => Db::raw("money + {$qSettle}"), +]); +$mNewMoney = (float)Db::name('master')->where('id', $masterId)->value('money'); +assertTest(round($mNewMoney - $mOldMoney, 2) == $qSettle, "客户线上自主点击确认验收成功,师傅入账 ¥{$qSettle},报价竞价流程完美闭环!\n"); + + +// ----------------------------------------------------------------------------- +// 用例三:超时未验收自动核销兜底机制测试(防止师傅被压款) +// ----------------------------------------------------------------------------- +echo ">> [用例三] 超时未验收自动核销兜底机制测试:\n"; + +$autoOrderNo = 'AUTO' . date('YmdHis') . mt_rand(100, 999); +$autoOrderId = Db::name('order')->insertGetId([ + 'order_no' => $autoOrderNo, + 'user_id' => $userId, + 'master_id' => $masterId, + 'cat_id' => 1, + 'service_name' => '智能锁更换与调试', + 'price_type' => 'fixed', + 'goods_num' => 1, + 'contact' => '失联出差业主', + 'mobile' => '13800000003', + 'address' => '白云区金阳南路', + 'base_amount' => 100, + 'total_amount' => 100, + 'pay_amount' => 100, + 'status' => OrderStatus::PENDING_VERIFY, // 待验收 + 'pay_status' => 1, + 'finish_time' => time() - 173000, // 完工超过 48 小时 + 'settle_status'=> 0, + 'createtime' => time() - 200000, + 'updatetime' => time(), +]); +assertTest($autoOrderId > 0, "构造完工超48小时且业主未确认的工单 ID: {$autoOrderId}"); + +// 触发自动核销 +$autoCount = Order::autoVerifyExpired(172800); +assertTest($autoCount >= 1, "触发 Order::autoVerifyExpired(),成功自动核销 {$autoCount} 笔超时工单"); + +$autoOrderCheck = Db::name('order')->where('id', $autoOrderId)->find(); +assertTest((int)$autoOrderCheck['status'] === OrderStatus::COMPLETED && (int)$autoOrderCheck['settle_status'] === 1, + "超48小时工单已自动流转为已完成(8),结算状态: 1,成功兜底保障师傅权益!\n"); + +echo "========================================================\n"; +echo " 🎉 自动化测试套件执行完毕:全部用例 100% 通过! \n"; +echo "========================================================\n"; diff --git a/server/addons/cos/config.php b/server/addons/cos/config.php index 48e41e3..eb3c3bb 100644 --- a/server/addons/cos/config.php +++ b/server/addons/cos/config.php @@ -1,257 +1,313 @@ 'appId', - 'title' => 'AppID', - 'type' => 'string', - 'content' => [], - 'value' => '1234567890', - 'rule' => 'required', - 'msg' => '', - 'tip' => '请前往腾讯控制台 > 访问管理 > API密钥', - 'ok' => '', - 'extend' => '', - ], - [ - 'name' => 'secretId', - 'title' => 'SecretId', - 'type' => 'string', - 'content' => [], - 'value' => '', - 'rule' => 'required', - 'msg' => '', - 'tip' => '请前往腾讯控制台 > 访问管理 > API密钥', - 'ok' => '', - 'extend' => '', - ], - [ - 'name' => 'secretKey', - 'title' => 'SecretKey', - 'type' => 'string', - 'content' => [], - 'value' => '', - 'rule' => 'required', - 'msg' => '', - 'tip' => '请前往腾讯控制台 > 访问管理 > API密钥', - 'ok' => '', - 'extend' => '', - ], - [ - 'name' => 'bucket', - 'title' => '存储桶名称', - 'type' => 'string', - 'content' => [], - 'value' => 'yourbucket-1234567890', - 'rule' => 'required;bucket', - 'msg' => '', - 'tip' => '存储空间名称', - 'ok' => '', - 'extend' => 'data-rule-bucket="[/^[0-9a-z_\-]{3,63}$/, \'请输入正确的存储桶名称\']"', - ], - [ - 'name' => 'region', - 'title' => '地域名称', - 'type' => 'string', - 'content' => [], - 'value' => 'ap-guangzhou', - 'rule' => 'required', - 'msg' => '', - 'tip' => '请输入地域简称,请注意使用英文', - 'ok' => '', - 'extend' => '', - ], - [ - 'name' => 'uploadmode', - 'title' => '上传模式', - 'type' => 'select', - 'content' => [ - 'client' => '客户端直传(速度快,无备份)', - 'server' => '服务器中转(占用服务器带宽,有备份)', - ], - 'value' => 'server', - 'rule' => '', - 'msg' => '', - 'tip' => '', - 'ok' => '', - 'extend' => '', - ], - [ - 'name' => 'serverbackup', - 'title' => '服务器中转模式备份', - 'type' => 'radio', - 'content' => [ - 1 => '备份(附件管理将产生2条记录)', - 0 => '不备份', - ], - 'value' => '1', - 'rule' => '', - 'msg' => '', - 'tip' => '服务器中转模式下是否备份文件', - 'ok' => '', - 'extend' => '', - ], - [ - 'name' => 'uploadurl', - 'title' => '上传接口地址', - 'type' => 'string', - 'content' => [], - 'value' => '', - 'rule' => 'required;uploadurl', - 'msg' => '', - 'tip' => '请输入你的上传接口地址', - 'ok' => '', - 'extend' => 'data-rule-uploadurl="[/^http(s)?:\/\/.*$/, \'必需以http(s)://开头\']"', - ], - [ - 'name' => 'cdnurl', - 'title' => 'CDN地址', - 'type' => 'string', - 'content' => [], - 'value' => '', - 'rule' => 'required;cdnurl', - 'msg' => '', - 'tip' => '请配置你的CDN地址或在存储桶基础配置中获取', - 'ok' => '', - 'extend' => 'data-rule-cdnurl="[/^http(s)?:\/\/.*$/, \'必需以http(s)://开头\']"', - ], - [ - 'name' => 'savekey', - 'title' => '保存文件名', - 'type' => 'string', - 'content' => [], - 'value' => '/{category}/{year}{mon}{day}/{filemd5}{.suffix}', - 'rule' => 'required', - 'msg' => '', - 'tip' => '', - 'ok' => '', - 'extend' => '', - ], - [ - 'name' => 'expire', - 'title' => '上传有效时长', - 'type' => 'string', - 'content' => [], - 'value' => '600', - 'rule' => 'required', - 'msg' => '', - 'tip' => '用户停留页面上传有效时长,单位秒', - 'ok' => '', - 'extend' => '', - ], - [ - 'name' => 'maxsize', - 'title' => '最大可上传', - 'type' => 'string', - 'content' => [], - 'value' => '10M', - 'rule' => 'required', - 'msg' => '', - 'tip' => '', - 'ok' => '', - 'extend' => '', - ], - [ - 'name' => 'mimetype', - 'title' => '可上传后缀格式', - 'type' => 'string', - 'content' => [], - 'value' => 'jpg,png,bmp,jpeg,gif,zip,rar', - 'rule' => 'required', - 'msg' => '', - 'tip' => '', - 'ok' => '', - 'extend' => '', - ], - [ - 'name' => 'multiple', - 'title' => '多文件上传', - 'type' => 'bool', - 'content' => [], - 'value' => '0', - 'rule' => 'required', - 'msg' => '', - 'tip' => '', - 'ok' => '', - 'extend' => '', - ], - [ - 'name' => 'thumbstyle', - 'title' => '缩略图样式', - 'type' => 'string', - 'content' => [], - 'value' => '', - 'rule' => '', - 'msg' => '', - 'tip' => '用于后台列表缩略图样式,可使用:?imageMogr2/thumbnail/120x90', - 'ok' => '', - 'extend' => '', - ], - [ - 'name' => 'chunking', - 'title' => '分片上传', - 'type' => 'radio', - 'content' => [ - 1 => '开启', - 0 => '关闭', - ], - 'value' => '0', - 'rule' => 'required', - 'msg' => '', - 'tip' => '', - 'ok' => '', - 'extend' => '', - ], - [ - 'name' => 'chunksize', - 'title' => '分片大小', - 'type' => 'number', - 'content' => [], - 'value' => '4194304', - 'rule' => 'required', - 'msg' => '', - 'tip' => '', - 'ok' => '', - 'extend' => '', - ], - [ - 'name' => 'syncdelete', - 'title' => '附件删除时是否同步删除云存储文件', - 'type' => 'bool', - 'content' => [], - 'value' => '0', - 'rule' => 'required', - 'msg' => '', - 'tip' => '', - 'ok' => '', - 'extend' => '', - ], - [ - 'name' => 'apiupload', - 'title' => 'API接口使用云存储', - 'type' => 'bool', - 'content' => [], - 'value' => '0', - 'rule' => 'required', - 'msg' => '', - 'tip' => '', - 'ok' => '', - 'extend' => '', - ], - [ - 'name' => 'noneedlogin', - 'title' => '免登录上传', - 'type' => 'checkbox', - 'content' => [ - 'api' => 'API', - 'index' => '前台', - 'admin' => '后台', - ], - 'value' => '', - 'rule' => '', - 'msg' => '', - 'tip' => '', - 'ok' => '', - 'extend' => '', - ], -]; +return array ( + 0 => + array ( + 'name' => 'appId', + 'title' => 'AppID', + 'type' => 'string', + 'content' => + array ( + ), + 'value' => '1512102519', + 'rule' => 'required', + 'msg' => '', + 'tip' => '请前往腾讯控制台 > 访问管理 > API密钥', + 'ok' => '', + 'extend' => '', + ), + 1 => + array ( + 'name' => 'secretId', + 'title' => 'SecretId', + 'type' => 'string', + 'content' => + array ( + ), + 'value' => 'AKIDxOo34sUu892lWFoEnFVYS9A58D84pa8X', + 'rule' => 'required', + 'msg' => '', + 'tip' => '请前往腾讯控制台 > 访问管理 > API密钥', + 'ok' => '', + 'extend' => '', + ), + 2 => + array ( + 'name' => 'secretKey', + 'title' => 'SecretKey', + 'type' => 'string', + 'content' => + array ( + ), + 'value' => 'IMDwjS5Xpwtc9WiV7dz95rTvHUbYJtSv', + 'rule' => 'required', + 'msg' => '', + 'tip' => '请前往腾讯控制台 > 访问管理 > API密钥', + 'ok' => '', + 'extend' => '', + ), + 3 => + array ( + 'name' => 'bucket', + 'title' => '存储桶名称', + 'type' => 'string', + 'content' => + array ( + ), + 'value' => 'yjyf-1512102519', + 'rule' => 'required;bucket', + 'msg' => '', + 'tip' => '存储空间名称', + 'ok' => '', + 'extend' => 'data-rule-bucket="[/^[0-9a-z_\\-]{3,63}$/, \'请输入正确的存储桶名称\']"', + ), + 4 => + array ( + 'name' => 'region', + 'title' => '地域名称', + 'type' => 'string', + 'content' => + array ( + ), + 'value' => 'ap-guangzhou', + 'rule' => 'required', + 'msg' => '', + 'tip' => '请输入地域简称,请注意使用英文', + 'ok' => '', + 'extend' => '', + ), + 5 => + array ( + 'name' => 'uploadmode', + 'title' => '上传模式', + 'type' => 'select', + 'content' => + array ( + 'client' => '客户端直传(速度快,无备份)', + 'server' => '服务器中转(占用服务器带宽,有备份)', + ), + 'value' => 'server', + 'rule' => '', + 'msg' => '', + 'tip' => '', + 'ok' => '', + 'extend' => '', + ), + 6 => + array ( + 'name' => 'serverbackup', + 'title' => '服务器中转模式备份', + 'type' => 'radio', + 'content' => + array ( + 1 => '备份(附件管理将产生2条记录)', + 0 => '不备份', + ), + 'value' => '1', + 'rule' => '', + 'msg' => '', + 'tip' => '服务器中转模式下是否备份文件', + 'ok' => '', + 'extend' => '', + ), + 7 => + array ( + 'name' => 'uploadurl', + 'title' => '上传接口地址', + 'type' => 'string', + 'content' => + array ( + ), + 'value' => 'https://yjyf-1512102519.cos.ap-guangzhou.myqcloud.com', + 'rule' => 'required;uploadurl', + 'msg' => '', + 'tip' => '请输入你的上传接口地址', + 'ok' => '', + 'extend' => 'data-rule-uploadurl="[/^http(s)?:\\/\\/.*$/, \'必需以http(s)://开头\']"', + ), + 8 => + array ( + 'name' => 'cdnurl', + 'title' => 'CDN地址', + 'type' => 'string', + 'content' => + array ( + ), + 'value' => 'https://yjyf-1512102519.cos.ap-guangzhou.myqcloud.com', + 'rule' => 'required;cdnurl', + 'msg' => '', + 'tip' => '请配置你的CDN地址或在存储桶基础配置中获取', + 'ok' => '', + 'extend' => 'data-rule-cdnurl="[/^http(s)?:\\/\\/.*$/, \'必需以http(s)://开头\']"', + ), + 9 => + array ( + 'name' => 'savekey', + 'title' => '保存文件名', + 'type' => 'string', + 'content' => + array ( + ), + 'value' => '/{category}/{year}{mon}{day}/{filemd5}{.suffix}', + 'rule' => 'required', + 'msg' => '', + 'tip' => '', + 'ok' => '', + 'extend' => '', + ), + 10 => + array ( + 'name' => 'expire', + 'title' => '上传有效时长', + 'type' => 'string', + 'content' => + array ( + ), + 'value' => '600', + 'rule' => 'required', + 'msg' => '', + 'tip' => '用户停留页面上传有效时长,单位秒', + 'ok' => '', + 'extend' => '', + ), + 11 => + array ( + 'name' => 'maxsize', + 'title' => '最大可上传', + 'type' => 'string', + 'content' => + array ( + ), + 'value' => '10M', + 'rule' => 'required', + 'msg' => '', + 'tip' => '', + 'ok' => '', + 'extend' => '', + ), + 12 => + array ( + 'name' => 'mimetype', + 'title' => '可上传后缀格式', + 'type' => 'string', + 'content' => + array ( + ), + 'value' => 'jpg,png,bmp,jpeg,gif,zip,rar,mp4', + 'rule' => 'required', + 'msg' => '', + 'tip' => '', + 'ok' => '', + 'extend' => '', + ), + 13 => + array ( + 'name' => 'multiple', + 'title' => '多文件上传', + 'type' => 'bool', + 'content' => + array ( + ), + 'value' => '0', + 'rule' => 'required', + 'msg' => '', + 'tip' => '', + 'ok' => '', + 'extend' => '', + ), + 14 => + array ( + 'name' => 'thumbstyle', + 'title' => '缩略图样式', + 'type' => 'string', + 'content' => + array ( + ), + 'value' => '', + 'rule' => '', + 'msg' => '', + 'tip' => '用于后台列表缩略图样式,可使用:?imageMogr2/thumbnail/120x90', + 'ok' => '', + 'extend' => '', + ), + 15 => + array ( + 'name' => 'chunking', + 'title' => '分片上传', + 'type' => 'radio', + 'content' => + array ( + 1 => '开启', + 0 => '关闭', + ), + 'value' => '0', + 'rule' => 'required', + 'msg' => '', + 'tip' => '', + 'ok' => '', + 'extend' => '', + ), + 16 => + array ( + 'name' => 'chunksize', + 'title' => '分片大小', + 'type' => 'number', + 'content' => + array ( + ), + 'value' => '4194304', + 'rule' => 'required', + 'msg' => '', + 'tip' => '', + 'ok' => '', + 'extend' => '', + ), + 17 => + array ( + 'name' => 'syncdelete', + 'title' => '附件删除时是否同步删除云存储文件', + 'type' => 'bool', + 'content' => + array ( + ), + 'value' => '1', + 'rule' => 'required', + 'msg' => '', + 'tip' => '', + 'ok' => '', + 'extend' => '', + ), + 18 => + array ( + 'name' => 'apiupload', + 'title' => 'API接口使用云存储', + 'type' => 'bool', + 'content' => + array ( + ), + 'value' => '1', + 'rule' => 'required', + 'msg' => '', + 'tip' => '', + 'ok' => '', + 'extend' => '', + ), + 19 => + array ( + 'name' => 'noneedlogin', + 'title' => '免登录上传', + 'type' => 'checkbox', + 'content' => + array ( + 'api' => 'API', + 'index' => '前台', + 'admin' => '后台', + ), + 'value' => '', + 'rule' => '', + 'msg' => '', + 'tip' => '', + 'ok' => '', + 'extend' => '', + ), +); diff --git a/server/application/api/controller/Home.php b/server/application/api/controller/Home.php index 08472f1..6551783 100644 --- a/server/application/api/controller/Home.php +++ b/server/application/api/controller/Home.php @@ -9,7 +9,7 @@ use think\Db; */ class Home extends Base { - protected $noNeedLogin = ['index', 'cats', 'services', 'detail', 'calc', 'specs', 'masterCount']; + protected $noNeedLogin = ['index', 'cats', 'services', 'detail', 'calc', 'specs', 'masterCount', 'orderCase']; /** * 首页聚合:类目 + 热门服务 + 我的角标 @@ -23,16 +23,73 @@ class Home extends Base ->select(); $cats = $this->formatCats($rawCats); - $hot = Db::name('service') - ->field('id,cat_id,name,price_type,base_price,max_price,unit,image,sales') - ->where('status', 'normal') - ->order('sales desc,id asc') - ->limit(6) + // 热门服务多维推荐计算: + // 维度1: 真实完工订单量 (fa_order status=8) + // 维度2: 基础销量 sales + // 维度3: 明码标价一口价优先 (fixed) + // 维度4: 品类均衡覆盖 (避免单一类目倾斜,各高频类目精选优质项目) + // 维度5: 后台置顶权重 weigh + $orderCounts = Db::name('order') + ->where('status', 8) + ->group('service_id') + ->column('count(*)', 'service_id'); + + $candidateServices = Db::name('service') + ->alias('s') + ->join('service_cat c', 's.cat_id = c.id', 'LEFT') + ->field('s.id,s.cat_id,s.name,s.price_type,s.base_price,s.max_price,s.unit,s.image,s.sales,s.weigh,c.name as cat_name,c.icon as cat_icon') + ->where('s.status', 'normal') + ->where('s.cat_id', '<>', 15) // 排除“其它”类目 ->select(); + // 综合打分 + foreach ($candidateServices as &$cs) { + $sid = (int)$cs['id']; + $realOrders = isset($orderCounts[$sid]) ? (int)$orderCounts[$sid] : 0; + $cs['real_orders'] = $realOrders; + $cs['score'] = ($realOrders * 15) + ((int)$cs['sales'] * 2) + ($cs['price_type'] === 'fixed' ? 25 : 0) + (int)$cs['weigh']; + + // 推荐标签 + if ($realOrders > 0) { + $cs['tag'] = '热销爆款'; + } elseif ($cs['price_type'] === 'fixed') { + $cs['tag'] = '明码标价'; + } else { + $cs['tag'] = '好评优选'; + } + } + unset($cs); + + // 按得分降序排列 + usort($candidateServices, function($a, $b) { + return $b['score'] <=> $a['score']; + }); + + // 品类均衡采样:每个类目优先入选 1 个,最多 2 个,输出 6~8 项 + $hotList = []; + $catSeen = []; + // 第一轮:每个类目选最热 1 项 + foreach ($candidateServices as $cs) { + $cid = (int)$cs['cat_id']; + if (!isset($catSeen[$cid])) { + $hotList[] = $cs; + $catSeen[$cid] = 1; + } + if (count($hotList) >= 6) break; + } + // 若不足 6 项,允许热门类目补充第 2 项 + if (count($hotList) < 6) { + foreach ($candidateServices as $cs) { + if (!in_array($cs['id'], array_column($hotList, 'id'), true)) { + $hotList[] = $cs; + } + if (count($hotList) >= 6) break; + } + } + $data = [ 'cats' => $cats, - 'hot' => $this->formatServices($hot), + 'hot' => $this->formatServices($hotList), 'banners' => $this->banners(), 'assures' => $this->configList('home_assure', '平台认证,先行赔付,极速上门,售后无忧'), 'tutorial' => $this->tutorial(), @@ -45,6 +102,84 @@ class Home extends Base $data['badge']['unread'] = (int)Db::name('message')->where('receive_type', 'user')->where('receive_id', $uid)->where('is_read', 0)->count(); } + // 全平台真实已完工工单动态流(严格只展示已完工 status = 8) + $orderRows = Db::name('order') + ->alias('o') + ->join('service s', 'o.service_id = s.id', 'LEFT') + ->join('service_cat c', 'o.cat_id = c.id', 'LEFT') + ->field('o.id,o.order_no,o.service_id,o.service_name,o.cat_id,o.price_type,o.pay_amount,o.status,o.appoint_time,o.finish_time,o.verify_time,o.createtime,o.city,o.district,o.address,o.service_image,o.goods_images,s.image as s_image,c.name as c_name,c.icon as cat_icon') + ->where('o.status', 8) + ->order('o.id desc') + ->limit(10) + ->select(); + + $statusTextMap = [ + 8 => '已完工', + ]; + + $recentOrders = []; + foreach ($orderRows as $or) { + $st = (int)$or['status']; + $appoint = (int)$or['appoint_time']; + $finishTime = !empty($or['finish_time']) ? (int)$or['finish_time'] : (!empty($or['verify_time']) ? (int)$or['verify_time'] : (int)$or['createtime']); + + // 图片优先级:订单快照服务图 > 服务表图 > 现场照片第一张 > 类目图标 + $img = ''; + if (!empty($or['service_image'])) { + $img = $or['service_image']; + } elseif (!empty($or['s_image'])) { + $img = $or['s_image']; + } elseif (!empty($or['goods_images'])) { + $gImgs = is_array($or['goods_images']) ? $or['goods_images'] : explode(',', (string)$or['goods_images']); + $img = reset($gImgs); + } elseif (!empty($or['cat_icon'])) { + $img = $or['cat_icon']; + } + $imageUrl = $img ? cdnurl($img, true) : ''; + + // 区域脱敏:提取有效片区,剔除详细门牌号与连续数字 + $city = trim((string)$or['city']); + $district = trim((string)$or['district']); + $address = trim((string)$or['address']); + $cleanAddr = $address; + if ($cleanAddr !== '') { + // 剔除前缀省市区冗余 + $cleanAddr = preg_replace('/^(?:.*?省)?(?:.*?市)?(?:.*?区)?/u', '', $cleanAddr); + // 剔除房间号、单元号、楼栋号 + $cleanAddr = preg_replace('/[0-9]+[室号单元栋层].*$/u', '', $cleanAddr); + // 剔除末尾连续3位以上门牌数字 + $cleanAddr = preg_replace('/[0-9]{3,}$/u', '', $cleanAddr); + $cleanAddr = trim($cleanAddr, " -_·"); + } + $locPrefix = $district ?: $city; + if ($locPrefix && $cleanAddr) { + $loc = $locPrefix . ' · ' . $cleanAddr; + } elseif ($cleanAddr) { + $loc = $cleanAddr; + } elseif ($locPrefix) { + $loc = $locPrefix; + } else { + $loc = '同城服务'; + } + + $recentOrders[] = [ + 'id' => (int)$or['id'], + 'order_no' => $or['order_no'], + 'service_id' => (int)$or['service_id'], + 'service_name' => $or['service_name'] ?: '上门服务', + 'cat_name' => isset($or['c_name']) && $or['c_name'] ? $or['c_name'] : '服务', + 'cat_id' => (int)$or['cat_id'], + 'image_url' => $imageUrl, + 'status' => $st, + 'status_text' => '已完工', + 'pay_amount' => (float)$or['pay_amount'], + 'location' => $loc, + 'appoint_text' => $appoint ? date('m-d H:i', $appoint) : '', + 'time_ago' => $this->timeAgo($finishTime), + ]; + } + $data['recent_orders'] = $recentOrders; + $this->success('', $data); } @@ -185,10 +320,31 @@ class Home extends Base public function calc() { $serviceId = (int)$this->request->param('service_id'); - $goodsListRaw = $this->request->param('goods_list/a'); + $goodsListRaw = $this->request->param('goods_list', '', null); $goodsList = []; if (!empty($goodsListRaw)) { - $goodsList = is_array($goodsListRaw) ? $goodsListRaw : json_decode((string)$goodsListRaw, true); + 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; + } + } + } + + if ($serviceId <= 0 && !empty($goodsList) && is_array($goodsList)) { + $first = reset($goodsList); + if (is_array($first) && !empty($first['service_id'])) { + $serviceId = (int)$first['service_id']; + } } $service = null; @@ -228,7 +384,20 @@ class Home extends Base $base = 0; if (!empty($goodsList) && is_array($goodsList)) { foreach ($goodsList as $g) { + if (is_string($g)) { + $rawG = htmlspecialchars_decode($g); + $decoded = json_decode($rawG, true); + if (is_array($decoded)) { + $g = $decoded; + } + } + if (!is_array($g)) { + continue; + } $itemPrice = isset($g['base_price']) ? (float)$g['base_price'] : 0; + if ($itemPrice <= 0 && !empty($g['service_id'])) { + $itemPrice = (float)Db::name('service')->where('id', (int)$g['service_id'])->value('base_price'); + } $itemNum = isset($g['num']) ? max(1, (int)$g['num']) : 1; $base += $itemPrice * $itemNum; if (!empty($g['extras']) && is_array($g['extras'])) { @@ -307,7 +476,7 @@ class Home extends Base ->limit(8) ->select(); if (!$rows) { - return $this->defaultBanners(); + return []; } $themeText = [ @@ -486,6 +655,9 @@ class Home extends Base { if (empty($cats)) return []; foreach ($cats as &$c) { + if ((int)$c['id'] === 15 || $c['name'] === '其它' || $c['name'] === '其他') { + $c['name'] = '全部分类'; + } $icon = (string)$c['icon']; if ($icon !== '') { if (strpos($icon, 'http') === 0) { @@ -554,13 +726,16 @@ class Home extends Base } } + $catIcon = isset($row['cat_icon']) && $row['cat_icon'] ? $row['cat_icon'] : ''; + $img = $row['image'] ? $row['image'] : $catIcon; + return [ 'id' => (int)$row['id'], 'cat_id' => (int)$row['cat_id'], 'name' => $row['name'], 'cat_name' => isset($row['cat_name']) ? $row['cat_name'] : '', 'image' => $row['image'], - 'image_url' => $row['image'] ? cdnurl($row['image'], true) : '', + 'image_url' => $img ? cdnurl($img, true) : '', 'price_type' => $type, 'price_type_text' => $type === 'fixed' ? '一口价' : ($type === 'quote' ? '师傅报价' : '按面积计价'), 'base_price' => (float)$row['base_price'], @@ -572,8 +747,231 @@ class Home extends Base 'free_floor' => isset($row['free_floor']) ? (int)$row['free_floor'] : 0, 'need_model' => isset($row['need_model']) ? (int)$row['need_model'] : 1, 'description' => isset($row['description']) ? $row['description'] : '', + 'tag' => isset($row['tag']) && $row['tag'] ? $row['tag'] : '明码标价', 'service_types' => $serviceTypes, 'addons' => $addons, ]; } + + /** + * 相对时间计算(更具真实动态感) + */ + protected function timeAgo($time) + { + if ($time <= 0) return ''; + $diff = time() - $time; + if ($diff < 60) return '刚刚'; + if ($diff < 3600) return floor($diff / 60) . '分钟前'; + if ($diff < 86400) return floor($diff / 3600) . '小时前'; + if ($diff < 86400 * 3) return floor($diff / 86400) . '天前'; + return date('m-d', $time); + } + + /** + * 公开工单案例详情(含脱敏订单信息、现场施工、师傅名片与真实评价) + */ + public function orderCase() + { + $id = (int)$this->request->param('id'); + if ($id <= 0) { + $this->error('工单参数错误'); + } + + $order = Db::name('order')->where('id', $id)->find(); + if (!$order) { + $this->error('工单案例不存在'); + } + + $service = Db::name('service')->where('id', (int)$order['service_id'])->find(); + $cat = Db::name('service_cat')->where('id', (int)$order['cat_id'])->find(); + + // 订单状态文本映射 + $statusMap = [ + 0 => '待支付', 1 => '待接单', 2 => '待确认', 3 => '已接单', + 4 => '待上门', 5 => '已到达', 6 => '施工中', 7 => '待验收', 8 => '已完成' + ]; + $st = (int)$order['status']; + $statusText = isset($statusMap[$st]) ? $statusMap[$st] : '已完成'; + + // 封面图提取 + $img = ''; + if ($service && !empty($service['image'])) { + $img = $service['image']; + } elseif (!empty($order['goods_image'])) { + $img = $order['goods_image']; + } elseif (!empty($order['repair_images'])) { + $repArr = is_string($order['repair_images']) ? explode(',', $order['repair_images']) : []; + $img = !empty($repArr[0]) ? $repArr[0] : ''; + } elseif ($cat && !empty($cat['icon'])) { + $img = $cat['icon']; + } + $imageUrl = $img ? cdnurl($img, true) : ''; + + // 区域脱敏 + $city = trim((string)$order['city']); + $district = trim((string)$order['district']); + $address = trim((string)$order['address']); + $cleanAddr = $address; + if ($cleanAddr !== '') { + $cleanAddr = preg_replace('/^(?:.*?省)?(?:.*?市)?(?:.*?区)?/u', '', $cleanAddr); + $cleanAddr = preg_replace('/[0-9]+[室号单元栋层].*$/u', '', $cleanAddr); + $cleanAddr = preg_replace('/[0-9]{3,}$/u', '', $cleanAddr); + $cleanAddr = trim($cleanAddr, " -_·"); + } + $locPrefix = $district ?: $city; + if ($locPrefix && $cleanAddr) { + $loc = $locPrefix . ' · ' . $cleanAddr; + } elseif ($cleanAddr) { + $loc = $cleanAddr; + } elseif ($locPrefix) { + $loc = $locPrefix; + } else { + $loc = '同城上门服务'; + } + + // 订单号脱敏 + $orderNo = (string)$order['order_no']; + if (strlen($orderNo) > 10) { + $maskedOrderNo = substr($orderNo, 0, 6) . '****' . substr($orderNo, -4); + } else { + $maskedOrderNo = $orderNo; + } + + // 商品列表 + $goodsRows = Db::name('order_goods')->where('order_id', $id)->select(); + $goodsList = []; + foreach ($goodsRows as $gr) { + $goodsList[] = [ + 'id' => (int)$gr['id'], + 'goods_name' => $gr['goods_name'], + 'goods_model' => $gr['goods_model'], + 'goods_num' => (int)$gr['goods_num'], + 'addon_services' => $gr['addon_services'], + 'base_price' => (float)$gr['base_price'], + ]; + } + + // 施工现场媒体图片 + $mediaRows = Db::name('order_media')->where('order_id', $id)->order('id asc')->select(); + $mediaList = []; + foreach ($mediaRows as $mr) { + $mUrl = $mr['url'] ? cdnurl($mr['url'], true) : ''; + if ($mUrl) { + $mediaList[] = [ + 'id' => (int)$mr['id'], + 'type' => $mr['type'] ?: 'image', + 'url' => $mUrl, + 'tag' => $mr['tag'] ?: '施工实况', + ]; + } + } + // 如果 order_media 为空,但有 repair_images,补充展示 + if (empty($mediaList) && !empty($order['repair_images'])) { + $rArr = explode(',', $order['repair_images']); + foreach ($rArr as $rIdx => $rImg) { + if ($rImg) { + $mediaList[] = [ + 'id' => $rIdx + 1, + 'type' => 'image', + 'url' => cdnurl(trim($rImg), true), + 'tag' => '现场实况', + ]; + } + } + } + + // 师傅信息(脱敏展示) + $master = null; + if ((int)$order['master_id'] > 0) { + $m = Db::name('master')->where('id', $order['master_id'])->find(); + if ($m) { + $mName = $m['realname'] ?: '优匠师傅'; + $master = [ + 'id' => (int)$m['id'], + 'realname' => mb_substr($mName, 0, 1) . '师傅', + 'score' => (float)($m['score'] > 0 ? $m['score'] : 5.0), + 'order_count' => (int)$m['order_count'], + 'avatar' => $m['avatar'] ? cdnurl($m['avatar'], true) : '', + ]; + } + } + + // 真实评价信息 + $reviewRow = Db::name('review')->where('order_id', $id)->where('status', 'normal')->find(); + $review = null; + if ($reviewRow) { + $u = Db::name('user')->where('id', (int)$reviewRow['user_id'])->find(); + $uname = '客户'; + if ((int)$reviewRow['is_anonymous'] === 1) { + $uname = '匿名客户'; + } elseif ($u && !empty($u['nickname'])) { + $uname = mb_substr($u['nickname'], 0, 1) . '**'; + } + $tags = []; + if (!empty($reviewRow['tags'])) { + $dec = json_decode($reviewRow['tags'], true); + $tags = is_array($dec) ? $dec : array_filter(explode(',', $reviewRow['tags'])); + } + $rImgs = []; + if (!empty($reviewRow['images'])) { + $decImgs = json_decode($reviewRow['images'], true); + $rawImgArr = is_array($decImgs) ? $decImgs : array_filter(explode(',', $reviewRow['images'])); + foreach ($rawImgArr as $imgItem) { + if ($imgItem) $rImgs[] = cdnurl(trim($imgItem), true); + } + } + $review = [ + 'id' => (int)$reviewRow['id'], + 'score' => (int)$reviewRow['score'], + 'tags' => array_values($tags), + 'content' => $reviewRow['content'] ?: '师傅很专业,准时上门,施工规范细致,验收很满意!', + 'images' => array_values($rImgs), + 'reply' => (string)$reviewRow['reply'], + 'user_name' => $uname, + 'user_avatar' => ($u && !(int)$reviewRow['is_anonymous'] && $u['avatar']) ? cdnurl($u['avatar'], true) : '', + 'time_text' => date('Y-m-d', (int)$reviewRow['createtime']), + ]; + } + + $goodsName = ''; + if (!empty($goodsList)) { + $goodsName = $goodsList[0]['goods_name']; + } + if (!$goodsName) { + $goodsName = $order['goods_name'] ?: $order['service_name']; + } + $catName = $cat ? $cat['name'] : ($order['cat_name'] ?: '优选品类'); + + $this->success('', [ + 'order' => [ + 'id' => (int)$order['id'], + 'order_no' => $maskedOrderNo, + 'service_id' => (int)$order['service_id'], + 'service_name' => $order['service_name'] ?: '上门服务', + 'cat_id' => (int)$order['cat_id'], + 'cat_name' => $catName, + 'goods_name' => $goodsName, + 'image_url' => $imageUrl, + 'status' => $st, + 'status_text' => $statusText, + 'pay_amount' => (float)$order['pay_amount'], + 'location' => $loc, + 'appoint_text' => $order['appoint_time'] ? date('Y-m-d H:i', (int)$order['appoint_time']) : '', + 'create_text' => date('Y-m-d H:i', (int)$order['createtime']), + 'finish_text' => !empty($order['finish_time']) ? date('Y-m-d H:i', (int)$order['finish_time']) : (!empty($order['verify_time']) ? date('Y-m-d H:i', (int)$order['verify_time']) : ''), + 'goods_list' => $goodsList, + 'media' => $mediaList, + ], + 'master' => $master, + 'review' => $review, + 'service_info' => [ + 'cat_id' => (int)$order['cat_id'], + 'cat_name' => $catName, + 'service_id' => (int)$order['service_id'], + 'service_name' => $order['service_name'] ?: '上门服务', + 'goods_name' => $goodsName, + 'base_price' => (float)$order['pay_amount'], + ] + ]); + } } diff --git a/server/application/api/controller/Order.php b/server/application/api/controller/Order.php index 701da70..ce769db 100644 --- a/server/application/api/controller/Order.php +++ b/server/application/api/controller/Order.php @@ -41,10 +41,24 @@ class Order extends Base $goodsImages = $this->request->post('goods_images', ''); // 支持多商品组合 (goods_list) - $goodsListRaw = $this->request->post('goods_list'); + $goodsListRaw = $this->request->post('goods_list', '', null); $goodsList = []; if (!empty($goodsListRaw)) { - $goodsList = is_array($goodsListRaw) ? $goodsListRaw : json_decode((string)$goodsListRaw, true); + 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; @@ -341,18 +355,40 @@ class Order extends Base 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))); - // 注意:TP5 中 count() 执行后查询条件会被重置,必须重新构建查询 - $build = function () use ($status) { - $q = Db::name('order')->where('user_id', $this->uid()); + $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') { + } elseif ($status === 'completed' || $status === '8') { $q->where('status', OrderStatus::COMPLETED); - } elseif ($status === 'cancelled') { + } 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); } @@ -361,14 +397,50 @@ class Order extends Base $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('订单已删除'); + } + /** * 订单详情(含状态轨迹与师傅信息) */ @@ -449,9 +521,29 @@ class Order extends Base } } + $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' => $this->formatOrder($order), - 'goods_list' => Db::name('order_goods')->where('order_id', $orderId)->select(), + 'order' => $formattedOrder, + 'goods_list' => $goodsList, 'master' => $master, 'logs' => $logs, 'media' => $media, @@ -501,11 +593,19 @@ class Order extends Base $quoteAmount = round((float)$quote['amount'], 2); $payAmount = max(0, round($quoteAmount - (float)$order['discount_amount'], 2)); - $remark = '客户已选择 ' . $master['realname'] . ' 的报价 ¥' . $quoteAmount . ',等待师傅联系'; + $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 { - $this->transit($order, OrderStatus::ACCEPTED, $remark, [ + $updateData = [ 'master_id' => (int)$quote['master_id'], 'accept_time' => time(), 'base_amount' => $quoteAmount, @@ -513,7 +613,13 @@ class Order extends Base 'urgent_amount' => 0, 'total_amount' => $quoteAmount, 'pay_amount' => $payAmount, - ], 'user', $this->uid()); + '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) @@ -528,7 +634,11 @@ class Order extends Base $this->error('选择报价失败:' . $e->getMessage()); } - $this->success('已选定师傅,请等待师傅联系'); + $this->success('已选定师傅并完成支付托管,请等待师傅联系', [ + 'pay_amount' => $payAmount, + 'need_diff' => $needDiff, + 'already_pay' => $alreadyPaid, + ]); } /** @@ -689,28 +799,101 @@ class Order extends Base { $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), - '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(), - '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, + '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, ]); } @@ -753,5 +936,88 @@ class Order extends Base ]; 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; + } + +} diff --git a/server/application/common/library/Upload.php b/server/application/common/library/Upload.php index d28cdd3..a9c2106 100755 --- a/server/application/common/library/Upload.php +++ b/server/application/common/library/Upload.php @@ -192,6 +192,15 @@ class Upload $md5 = $md5 ? $md5 : (isset($this->fileInfo['tmp_name']) ? md5_file($this->fileInfo['tmp_name']) : ''); $category = $category ? $category : request()->post('category'); $category = $category ? xss_clean($category) : 'all'; + // 按登录用户分目录:从请求 token 解析 user_id,路径中插入 u{userid} 层级;未登录不插入 + $userId = 0; + $token = request()->server('HTTP_TOKEN', request()->request('token', \think\Cookie::get('token'))); + if ($token) { + $tokenInfo = Token::get($token); + if (!empty($tokenInfo['user_id'])) { + $userId = (int)$tokenInfo['user_id']; + } + } $replaceArr = [ '{year}' => date("Y"), '{mon}' => date("m"), @@ -201,7 +210,7 @@ class Upload '{sec}' => date("s"), '{random}' => Random::alnum(16), '{random32}' => Random::alnum(32), - '{category}' => $category ? $category : '', + '{category}' => $category . ($userId > 0 ? '/u' . $userId : ''), '{filename}' => substr($filename, 0, 100), '{fileprefix}' => substr($fileprefix, 0, 100), '{suffix}' => $suffix, diff --git a/server/application/config.php b/server/application/config.php index c995c7e..d980714 100755 --- a/server/application/config.php +++ b/server/application/config.php @@ -272,9 +272,9 @@ return [ //会员注册验证码类型email/mobile/wechat/text/false 'user_register_captcha' => 'text', //登录验证码 - 'login_captcha' => true, + 'login_captcha' => false, //登录失败超过10次则1天后重试 - 'login_failure_retry' => true, + 'login_failure_retry' => false, //是否同一账号同一时间只能在一个地方登录 'login_unique' => false, //是否开启IP变动检测 diff --git a/server/public/all/20261106/2cd8bde463f5d82aae0f0cec061d6b8f.png b/server/public/all/20261106/2cd8bde463f5d82aae0f0cec061d6b8f.png new file mode 100644 index 0000000..f37764b Binary files /dev/null and b/server/public/all/20261106/2cd8bde463f5d82aae0f0cec061d6b8f.png differ diff --git a/server/public/avatar/20261106/2cd8bde463f5d82aae0f0cec061d6b8f.png b/server/public/avatar/20261106/2cd8bde463f5d82aae0f0cec061d6b8f.png new file mode 100644 index 0000000..f37764b Binary files /dev/null and b/server/public/avatar/20261106/2cd8bde463f5d82aae0f0cec061d6b8f.png differ diff --git a/server/public/avatar/u104/20261106/2cd8bde463f5d82aae0f0cec061d6b8f.png b/server/public/avatar/u104/20261106/2cd8bde463f5d82aae0f0cec061d6b8f.png new file mode 100644 index 0000000..f37764b Binary files /dev/null and b/server/public/avatar/u104/20261106/2cd8bde463f5d82aae0f0cec061d6b8f.png differ diff --git a/yjyf-app/components/user/UserHomeView.vue b/yjyf-app/components/user/UserHomeView.vue index 9856ba2..977ee0f 100644 --- a/yjyf-app/components/user/UserHomeView.vue +++ b/yjyf-app/components/user/UserHomeView.vue @@ -1,48 +1,37 @@