field('id,name,icon') ->where('status', 'normal') ->order('weigh desc,id asc') ->select(); $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) ->select(); $data = [ 'cats' => $cats, 'hot' => $this->formatServices($hot), '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(); } $this->success('', $data); } /** * 全部类目 */ public function cats() { $cats = Db::name('service_cat') ->field('id,name,icon') ->where('status', 'normal') ->order('weigh desc,id asc') ->select(); $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 !== '') { $query->where('name', 'like', '%' . $keyword . '%'); } $list = $query->order('weigh desc,sales desc,id asc')->select(); $this->success('', ['list' => $this->formatServices($list)]); } /** * 服务详情 */ 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 calc() { $serviceId = (int)$this->request->param('service_id'); $service = Db::name('service')->where('id', $serviceId)->where('status', 'normal')->find(); if (!$service) { $this->error('服务不存在'); } $result = $this->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)); $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 用户自主定价金额(总价,含楼层/加急) */ public static function calcAmount($service, $params, $couponId = 0, $uid = 0, $priceType = '', $userPrice = 0) { $allowTypes = ['fixed', 'quote', 'range', 'custom']; $type = in_array($priceType, $allowTypes, true) ? $priceType : $service['price_type']; $base = (float)$service['base_price']; $floorAmount = 0; $urgentAmount = 0; if ($type === 'custom') { // 用户自主定价:出价即订单总价,已包含楼层费/加急费,师傅按此价接单 $base = max(0, round((float)$userPrice, 2)); $total = $base; } else { $floor = max(0, (int)$params['floor']); $freeFloor = (int)$service['free_floor']; $floorFee = (float)$service['floor_fee']; 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' => 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 : (int)$service['free_floor'], 'floor_fee' => $type === 'custom' ? 0 : round((float)$service['floor_fee'], 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 $this->defaultBanners(); } $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) { $ret = []; foreach ($list as $row) { $ret[] = $this->formatService($row); } return $ret; } /** * 单项格式化(价格文案统一由此输出) */ 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'] . ' · 师傅报价'; } 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'], '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'] : '', ]; } }