field('id,name,icon') ->where('status', 'normal') ->order('weigh desc,id asc') ->select(); $cats = $this->formatCats($rawCats); // 热门服务多维推荐计算: // 维度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($hotList), 'banners' => $this->banners(), 'assures' => $this->configList('home_assure', '平台认证,先行赔付,极速上门,售后无忧'), 'tutorial' => $this->tutorial(), 'badge' => ['coupon' => 0, 'unread' => 0], ]; if ($this->auth->isLogin()) { $uid = $this->uid(); $data['badge']['coupon'] = (int)Db::name('user_coupon')->where('user_id', $uid)->where('status', 'unused')->count(); $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']) && substr($or['service_image'], -4) !== '.svg') { $img = $or['service_image']; } elseif (!empty($or['goods_images'])) { $gImgs = is_array($or['goods_images']) ? $or['goods_images'] : explode(',', (string)$or['goods_images']); $firstG = reset($gImgs); if ($firstG && substr($firstG, -4) !== '.svg') { $img = $firstG; } } elseif (!empty($or['s_image']) && substr($or['s_image'], -4) !== '.svg') { $img = $or['s_image']; } // 若仍无实物图片,从同类目服务中获取真实高质量商品图,坚决不用矢量小图标 if (empty($img) || substr($img, -4) === '.svg') { $fallbackImg = Db::name('service') ->where('cat_id', (int)$or['cat_id']) ->where('image', '<>', '') ->where('image', 'not like', '%.svg%') ->order('weigh desc, id asc') ->value('image'); if ($fallbackImg) { $img = $fallbackImg; } } $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); } /** * 全部类目 */ public function cats() { $rawCats = Db::name('service_cat') ->field('id,name,icon') ->where('status', 'normal') ->order('weigh desc,id asc') ->select(); $cats = $this->formatCats($rawCats); foreach ($cats as &$c) { $icon = (string)$c['icon']; if ($icon !== '') { if (strpos($icon, 'http') === 0) { $c['icon_url'] = $icon; } elseif (strpos($icon, '/') !== false || strpos($icon, '.') !== false) { $c['icon_url'] = cdnurl($icon, true); } else { $c['icon_url'] = ''; } } else { $c['icon_url'] = ''; } } unset($c); $this->success('', ['list' => $cats]); } /** * 类目下的服务列表 */ public function services() { $catId = (int)$this->request->param('cat_id'); $keyword = trim((string)$this->request->param('keyword')); if ($catId <= 0 && $keyword === '') { $this->error('请选择服务类目'); } $query = Db::name('service')->where('status', 'normal'); if ($catId > 0) { $query->where('cat_id', $catId); } if ($keyword !== '') { $specMatchingIds = Db::name('service_spec') ->where('status', 'normal') ->where('name|description', 'like', '%' . $keyword . '%') ->column('service_id'); $query->where(function($q) use ($keyword, $specMatchingIds) { $q->where('name', 'like', '%' . $keyword . '%'); if (!empty($specMatchingIds)) { $q->whereOr('id', 'in', $specMatchingIds); } }); } $list = $query->order('weigh desc,sales desc,id asc')->select(); $this->success('', ['list' => $this->formatServices($list)]); } /** * 服务商品规格列表 */ public function specs() { $serviceId = (int)$this->request->param('service_id'); if ($serviceId <= 0) { $this->error('请指定服务商品'); } $specs = Db::name('service_spec') ->where('service_id', $serviceId) ->where('status', 'normal') ->order('weigh desc, id asc') ->select(); $ret = []; foreach ($specs as $sp) { $ret[] = [ 'id' => (int)$sp['id'], 'service_id' => (int)$sp['service_id'], 'name' => $sp['name'], 'description' => (string)$sp['description'], 'base_price' => (float)$sp['base_price'], ]; } $this->success('', ['list' => $ret]); } /** * 服务详情 */ public function detail() { $id = (int)$this->request->param('id'); $service = Db::name('service')->where('id', $id)->where('status', 'normal')->find(); if (!$service) { $this->error('服务不存在'); } $cat = Db::name('service_cat')->where('id', $service['cat_id'])->find(); $service['cat_name'] = $cat ? $cat['name'] : ''; $this->success('', ['detail' => $this->formatService($service)]); } /** * 区域师傅运力统计 */ public function masterCount() { $province = trim((string)$this->request->param('province', '')); $city = trim((string)$this->request->param('city', '')); $district = trim((string)$this->request->param('district', '')); $catId = (int)$this->request->param('cat_id', 0); $query = Db::name('master')->where('verify_status', 'pass'); if ($city !== '') { $query->where('city', 'like', '%' . $city . '%'); } $realCount = $query->count(); // 商业化基础运力感知(基于城市区县散列生成可信且稳定的运力规模,例如 150~1800+) $hashSeed = sprintf('%u', crc32($province . $city . $district)); $virtualBase = 150 + ($hashSeed % 1700); $total = max($realCount, $virtualBase); $this->success('', [ 'count' => (int)$total, 'region' => trim($city . ' ' . $district), 'tip' => "该地区可提供服务的师傅共 {$total} 位", ]); } /** * 费用试算(基础价 + 楼层费 + 加急费 - 优惠) * 支持下单页实时切换:一口价 / 师傅报价 / 用户自主定价 */ public function calc() { $serviceId = (int)$this->request->param('service_id'); $goodsListRaw = $this->request->param('goods_list', '', null); $goodsList = []; if (!empty($goodsListRaw)) { if (is_string($goodsListRaw)) { $rawStr = htmlspecialchars_decode($goodsListRaw); $decoded = json_decode($rawStr, true); if (is_array($decoded)) { $goodsList = $decoded; } } elseif (is_array($goodsListRaw)) { if (isset($goodsListRaw[0]) && is_string($goodsListRaw[0])) { $rawStr = htmlspecialchars_decode($goodsListRaw[0]); $decoded = json_decode($rawStr, true); $goodsList = is_array($decoded) ? $decoded : $goodsListRaw; } else { $goodsList = $goodsListRaw; } } } 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; if ($serviceId > 0) { $service = Db::name('service')->where('id', $serviceId)->where('status', 'normal')->find(); } $result = self::calcAmount($service, [ 'floor' => (int)$this->request->param('floor', 1), 'has_elevator' => (int)$this->request->param('has_elevator', 1), 'urgent' => (int)$this->request->param('urgent', 0), ], (int)$this->request->param('coupon_id', 0), $this->uid(), (string)$this->request->param('price_type', ''), (float)$this->request->param('user_price', 0), $goodsList); $this->success('', $result); } /** * 金额计算(下单与试算共用,保证前后端口径一致) * * @param array $service 服务行(可选) * @param array $params floor / has_elevator / urgent * @param int $couponId 用户优惠券 id * @param int $uid 用户 id * @param string $priceType 计价方式:fixed 一口价 / quote 师傅报价 / custom 用户自主定价 * @param float $userPrice 用户自主定价金额(总价,含楼层/加急) * @param array $goodsList 多组商品组合(含商品规格与增项拆旧等) */ public static function calcAmount($service, $params, $couponId = 0, $uid = 0, $priceType = '', $userPrice = 0, $goodsList = []) { $allowTypes = ['fixed', 'quote', 'range', 'custom']; $type = in_array($priceType, $allowTypes, true) ? $priceType : ($service ? $service['price_type'] : 'fixed'); // 支持多商品组合与增项计算 $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'])) { foreach ($g['extras'] as $ex) { $exPrice = isset($ex['price']) ? (float)$ex['price'] : 0; $exNum = isset($ex['num']) ? max(0, (int)$ex['num']) : 0; $base += $exPrice * $exNum; } } } } elseif ($service) { $base = (float)$service['base_price']; } $floorAmount = 0; $urgentAmount = 0; if ($type === 'custom') { // 用户自主定价:出价即订单总价,已包含楼层费/加急费,师傅按此价接单 $base = max(0, round((float)$userPrice, 2)); $total = $base; } elseif ($type === 'quote') { // 师傅报价模式:金额由师傅后续竞价给出,下单时总价与应付为 0 $total = 0; $floorAmount = 0; $urgentAmount = 0; } else { $floor = max(0, (int)$params['floor']); $freeFloor = $service ? (int)$service['free_floor'] : 2; $floorFee = $service ? (float)$service['floor_fee'] : 10.0; if ((int)$params['has_elevator'] === 0 && $floor > $freeFloor) { $floorAmount = ($floor - $freeFloor) * $floorFee; } $urgentAmount = (int)$params['urgent'] === 1 ? round($base * 0.2, 2) : 0; $total = round($base + $floorAmount + $urgentAmount, 2); } $discount = 0; $couponInfo = null; if ($couponId > 0 && $uid > 0) { $uc = Db::name('user_coupon')->where('id', $couponId)->where('user_id', $uid)->where('status', 'unused')->find(); if ($uc) { $coupon = Db::name('coupon')->where('id', $uc['coupon_id'])->where('status', 'normal')->find(); $notExpired = $coupon && (int)$coupon['end_time'] === 0 ? true : ($coupon && $coupon['end_time'] > time()); if ($notExpired && $total >= (float)$coupon['min_amount']) { $discount = $coupon['type'] === 'cash' ? (float)$coupon['amount'] : round($total * (1 - (float)$coupon['discount']), 2); $discount = min($discount, $total); $couponInfo = ['id' => (int)$uc['id'], 'name' => $coupon['name']]; } } } return [ 'price_type' => $type, 'user_price' => $type === 'custom' ? round($base, 2) : 0, 'base_amount' => $type === 'quote' ? 0 : round($base, 2), 'floor_amount' => round($floorAmount, 2), 'urgent_amount' => round($urgentAmount, 2), 'total_amount' => $total, 'discount_amount' => round($discount, 2), 'pay_amount' => max(0, round($total - $discount, 2)), 'coupon' => $couponInfo, 'free_floor' => $type === 'custom' ? 0 : ($service ? (int)$service['free_floor'] : 2), 'floor_fee' => $type === 'custom' ? 0 : round($service ? (float)$service['floor_fee'] : 10.0, 2), ]; } /** * 首页轮播(后台 fa_banner 可管,支持活动 / 平台实力 / 服务主题) */ protected function banners() { $rows = Db::name('banner') ->field('id,badge,title,subtitle,date_text,theme,action_text,stats,image,link,weigh') ->where('status', 'normal') ->order('weigh desc,id desc') ->limit(8) ->select(); if (!$rows) { return []; } $themeText = [ 'promo' => '活动促销', 'brand' => '平台实力', 'service' => '服务主题', ]; $ret = []; foreach ($rows as $r) { $theme = isset($themeText[$r['theme']]) ? $r['theme'] : 'promo'; $stats = $this->parseBannerStats(isset($r['stats']) ? $r['stats'] : ''); if ($theme === 'brand' && empty($stats)) { $stats = $this->defaultBrandStats(); } $actionText = trim((string)$r['action_text']); if ($actionText === '' && $theme !== 'brand') { $actionText = '立即查看'; } $ret[] = [ 'id' => (int)$r['id'], 'badge' => $r['badge'], 'title' => $r['title'], 'subtitle' => $r['subtitle'], 'date_text' => $r['date_text'], 'theme' => $theme, 'theme_text' => $themeText[$theme], 'action_text' => $actionText, 'stats' => $stats, 'image' => $r['image'], 'link' => $r['link'], ]; } return $ret; } /** * 解析平台实力主题的统计 JSON */ protected function parseBannerStats($raw) { $raw = trim((string)$raw); if ($raw === '') { return []; } $decoded = json_decode($raw, true); if (!is_array($decoded)) { return []; } $out = []; foreach ($decoded as $item) { if (!is_array($item) || !isset($item['value']) || !isset($item['label'])) { continue; } $out[] = [ 'value' => (string)$item['value'], 'label' => (string)$item['label'], ]; } return $out; } /** * 平台实力默认指标 */ protected function defaultBrandStats() { return [ ['value' => '460万+', 'label' => '专业师傅'], ['value' => '99.9%', 'label' => '区域覆盖到村镇'], ['value' => '1762万', 'label' => '成功订单'], ]; } /** * 无后台数据时的兜底轮播 */ protected function defaultBanners() { return [ [ 'id' => 0, 'badge' => '家装季', 'title' => '双11 狂欢大促', 'subtitle' => '下单立减 · 满额再返券', 'date_text' => '10.23 - 11.15', 'theme' => 'promo', 'theme_text' => '活动促销', 'action_text' => '立即抢购', 'stats' => [], 'image' => '', 'link' => '/pages/service/list?cat_id=1&cat_name=%E5%AE%B6%E5%85%B7', ], [ 'id' => -1, 'badge' => '平台实力', 'title' => '平台实力', 'subtitle' => '已有 176276994 个订单找到师傅', 'date_text' => '下单越多 · 越优惠', 'theme' => 'brand', 'theme_text' => '平台实力', 'action_text' => '', 'stats' => $this->defaultBrandStats(), 'image' => '', 'link' => '', ], [ 'id' => -2, 'badge' => '服务主题', 'title' => '家电清洗季', 'subtitle' => '空调 / 洗衣机 / 油烟机深度清洗', 'date_text' => '9.9 折起', 'theme' => 'service', 'theme_text' => '服务主题', 'action_text' => '立即预约', 'stats' => [], 'image' => '', 'link' => '/pages/service/list?cat_id=10&cat_name=%E5%AE%B6%E7%94%B5', ], ]; } /** * 读取 fa_config 中的逗号分隔列表 */ protected function configList($name, $default = '') { $v = Db::name('config')->where('name', $name)->value('value'); if (!$v) { $v = $default; } return array_values(array_filter(array_map('trim', explode(',', (string)$v)))); } /** * 首页教程卡(fa_config: 标题|副标题) */ protected function tutorial() { $v = Db::name('config')->where('name', 'home_tutorial')->value('value'); if (!$v) { return ['title' => '下单找师傅教程', 'sub' => '3 分钟看懂,下单不踩坑']; } $parts = explode('|', (string)$v); return [ 'title' => trim($parts[0]), 'sub' => isset($parts[1]) ? trim($parts[1]) : '', ]; } /** * 列表格式化 */ public function formatServices($list) { if (empty($list)) { return []; } $serviceIds = array_column($list, 'id'); $specsMap = []; if (!empty($serviceIds)) { $specs = Db::name('service_spec') ->where('service_id', 'in', $serviceIds) ->where('status', 'normal') ->order('weigh desc, id asc') ->select(); foreach ($specs as $sp) { $sid = (int)$sp['service_id']; if (!isset($specsMap[$sid])) { $specsMap[$sid] = []; } $specsMap[$sid][] = [ 'id' => (int)$sp['id'], 'service_id' => $sid, 'name' => $sp['name'], 'description' => (string)$sp['description'], 'base_price' => (float)$sp['base_price'], ]; } } $ret = []; foreach ($list as $row) { $formatted = $this->formatService($row); $formatted['specs'] = isset($specsMap[$row['id']]) ? $specsMap[$row['id']] : []; $ret[] = $formatted; } return $ret; } /** * 格式化类目列表(统一网络图片 URL) */ protected function formatCats($cats) { if (empty($cats)) return []; foreach ($cats as &$c) { $icon = (string)$c['icon']; if ($icon !== '') { if (strpos($icon, 'http') === 0) { $c['icon_url'] = $icon; } elseif (strpos($icon, '/') !== false || strpos($icon, '.') !== false) { $c['icon_url'] = cdnurl($icon, true); } else { $c['icon_url'] = ''; } } else { $c['icon_url'] = ''; } } unset($c); return $cats; } /** * 单项格式化(价格文案统一由此输出) */ public function formatService($row) { $type = $row['price_type']; $priceText = '¥' . (float)$row['base_price']; if ($type === 'range') { $priceText = '¥' . (float)$row['base_price'] . '-' . (float)$row['max_price'] . '/' . $row['unit']; } elseif ($type === 'quote') { $priceText = '¥' . (float)$row['base_price'] . ' · 师傅报价'; } // 解析工项 $serviceTypes = []; if (!empty($row['service_types'])) { $serviceTypes = is_array($row['service_types']) ? $row['service_types'] : json_decode($row['service_types'], true); } if (empty($serviceTypes)) { $serviceTypes = [ ['code' => 'install', 'name' => '安装', 'price' => (float)$row['base_price'], 'is_default' => 1], ['code' => 'repair', 'name' => '维修', 'price' => round((float)$row['base_price'] * 0.7, 2), 'is_default' => 0], ['code' => 'remove', 'name' => '拆旧', 'price' => round((float)$row['base_price'] * 0.5, 2), 'is_default' => 0], ['code' => 'deliver', 'name' => '送货到家', 'price' => 50.00, 'is_default' => 0], ]; } // 解析增项 $addons = []; if (!empty($row['addons'])) { $addons = is_array($row['addons']) ? $row['addons'] : json_decode($row['addons'], true); } if (empty($addons)) { // 获取类目通用增项 $catAddons = Db::name('service_addon') ->where('status', 'normal') ->where(function($q) use ($row) { $q->where('cat_id', $row['cat_id'])->whereOr('cat_id', 0); }) ->order('weigh desc, id asc') ->limit(8) ->select(); foreach ($catAddons as $ca) { $addons[] = [ 'id' => (int)$ca['id'], 'name' => $ca['name'], 'unit' => $ca['unit'], 'price' => (float)$ca['default_price'] ]; } } $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' => $img ? cdnurl($img, true) : '', 'price_type' => $type, 'price_type_text' => $type === 'fixed' ? '一口价' : ($type === 'quote' ? '师傅报价' : '按面积计价'), 'base_price' => (float)$row['base_price'], 'max_price' => (float)$row['max_price'], 'unit' => $row['unit'], 'price_text' => $priceText, 'sales' => (int)$row['sales'], 'floor_fee' => isset($row['floor_fee']) ? (float)$row['floor_fee'] : 0, '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 (!empty($order['service_image']) && substr($order['service_image'], -4) !== '.svg') { $img = $order['service_image']; } elseif (!empty($order['goods_images'])) { $gImgs = is_array($order['goods_images']) ? $order['goods_images'] : explode(',', (string)$order['goods_images']); $firstG = reset($gImgs); if ($firstG && substr($firstG, -4) !== '.svg') { $img = $firstG; } } elseif ($service && !empty($service['image']) && substr($service['image'], -4) !== '.svg') { $img = $service['image']; } if (empty($img) || substr($img, -4) === '.svg') { $fallbackImg = Db::name('service') ->where('cat_id', (int)$order['cat_id']) ->where('image', '<>', '') ->where('image', 'not like', '%.svg%') ->order('weigh desc, id asc') ->value('image'); if ($fallbackImg) { $img = $fallbackImg; } 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'], ] ]); } }