routeId(); if ($id <= 0) { $id = (int)$this->input('id', 0) ?: (int)$this->input('userId', 0); } if ($id <= 0) { $this->fail('缺少商家参数', 400); } // 优先按 merchant.id 查,若未命中再按 user_id 查 $m = Db::name('fc_merchant')->where('id', $id)->find(); if (!$m) { $m = Db::name('fc_merchant')->where('user_id', $id)->find(); } if (!$m) { $this->fail('商家门店不存在', 404); } // 关联发布者基础信息 $user = Db::name('user')->where('id', $m['user_id'])->find(); $userNickname = $user ? (string)$user['nickname'] : ''; $userAvatar = $user ? (string)$user['avatar'] : ''; // 统计真实有效发布数量 $postCount = (int)Db::name('fc_post') ->where('user_id', $m['user_id']) ->where('status', 1) ->where('audit_status', 1) ->whereNull('deletetime') ->count(); // 计算用户与门店之间的物理距离(km) $clientLat = $this->coord('lat', 90); $clientLng = $this->coord('lng', 180); $distance = null; if ($clientLat !== null && $clientLng !== null && !empty($m['lat']) && !empty($m['lng'])) { $distance = $this->calcDistance($clientLat, $clientLng, (float)$m['lat'], (float)$m['lng']); } // 首屏拉取前 6 条该商家的有效发帖 $posts = $this->fetchMerchantPosts($m['user_id'], 1, 6); // 商家详情页统一隐藏明文手机号(仅返回脱敏号码),通过 POST /merchant/:id/dial 校验每日拨打次数上限 $rawContact = trim((string)(isset($m['contact']) ? $m['contact'] : '')); $isMember = false; $isOwner = false; $me = $this->user(); if ($me) { $now = time(); $ext = $this->ext($me['id']); $isMember = (int)$ext['is_member'] === 1 && !empty($ext['member_expiretime']) && (int)$ext['member_expiretime'] > $now; $isOwner = (int)$m['user_id'] === (int)$me['id']; } $callState = $this->callQuotaState($me ? (int)$me['id'] : 0, $isMember); $maskedContact = $this->maskPhone($rawContact); $dialDisplay = $this->buildDialDisplay($userNickname ?: (string)$m['name'], $rawContact); $data = [ 'id' => (int)$m['id'], 'userId' => (int)$m['user_id'], 'name' => (string)$m['name'], 'logo' => (string)(isset($m['logo']) ? $m['logo'] : ''), 'banner' => (string)(isset($m['banner']) ? $m['banner'] : ''), 'address' => (string)(isset($m['address']) ? $m['address'] : ''), 'lat' => (isset($m['lat']) && $m['lat'] !== null && $m['lat'] !== '') ? (float)$m['lat'] : null, 'lng' => (isset($m['lng']) && $m['lng'] !== null && $m['lng'] !== '') ? (float)$m['lng'] : null, 'contact' => $maskedContact, 'hasPhone' => $rawContact !== '', 'dialDisplay' => $dialDisplay, 'callQuotaTotal' => $callState['total'], 'callQuotaUsed' => $callState['used'], 'callQuotaLeft' => $callState['left'], 'canDial' => $isOwner || $callState['unlimited'] || $callState['left'] > 0, 'unlocked' => false, 'category' => (string)(isset($m['category']) ? $m['category'] : ''), 'notice' => (string)(isset($m['notice']) ? $m['notice'] : ''), 'auditStatus' => (int)$m['audit_status'], 'verified' => (int)$m['audit_status'] === 1, 'licenseVerified' => !empty($m['license']) && (int)$m['audit_status'] === 1, 'postCount' => $postCount, 'distance' => $distance, 'userNickname' => $userNickname, 'userAvatar' => $userAvatar, 'createdAt' => isset($m['createtime']) ? $this->iso($m['createtime']) : '', 'posts' => $posts['list'], 'hasMorePosts' => $posts['hasMore'], ]; $this->ok($data); } /** * M1-B 商家详情页申请拨打电话(校验每日拨打次数上限并记录拨打日志) * POST /api/v1/merchant/:id/dial * → {phoneNumber, dialDisplay, callQuotaTotal, callQuotaUsed, callQuotaLeft} * * @return void */ public function dial() { $user = $this->auth(); $id = (int)$this->routeId(); if ($id <= 0) { $id = (int)$this->input('id', 0) ?: (int)$this->input('userId', 0); } if ($id <= 0) { $this->fail('缺少商家参数', 400); } $m = Db::name('fc_merchant')->where('id', $id)->find(); if (!$m) { $m = Db::name('fc_merchant')->where('user_id', $id)->find(); } if (!$m) { $this->fail('商家门店不存在', 404); } $phone = trim((string)(isset($m['contact']) ? $m['contact'] : '')); if ($phone === '') { $this->fail('该商家暂未预留联系电话'); } $now = time(); $ext = $this->ext($user['id']); $isMember = (int)$ext['is_member'] === 1 && !empty($ext['member_expiretime']) && (int)$ext['member_expiretime'] > $now; $isOwner = (int)$m['user_id'] === (int)$user['id']; $state = $this->callQuotaState((int)$user['id'], $isMember); if (!$isOwner && !$state['unlimited'] && $state['left'] <= 0) { $msg = $isMember ? ('您今日的 VIP 拨打次数已达上限(' . $state['total'] . '次/日)') : ('您今日的免费拨打次数已达上限(' . $state['total'] . '次/日),开通 VIP 会员可享每日更多拨打次数'); $this->fail($msg, 403); } $today = date('Y-m-d'); Db::name('fc_call_log')->insert([ 'user_id' => (int)$user['id'], 'target_type' => 'merchant', 'target_id' => (int)$m['id'], 'phone' => $phone, 'call_date' => $today, 'createtime' => $now, ]); $newUsed = $state['used'] + 1; $newLeft = $state['unlimited'] ? 999 : max(0, $state['total'] - $newUsed); $pubUser = Db::name('user')->where('id', $m['user_id'])->find(); $contactLabel = $pubUser && !empty($pubUser['nickname']) ? (string)$pubUser['nickname'] : (string)$m['name']; $this->ok([ 'phoneNumber' => $phone, 'dialDisplay' => $this->buildDialDisplay($contactLabel, $phone), 'callQuotaTotal' => $state['total'], 'callQuotaUsed' => $newUsed, 'callQuotaLeft' => $newLeft, ]); } /** * 构造免责声明拨打弹窗展示文案(如:王138****8000) * * @param string $name 联系人或门店名称 * @param string $phone 手机号 * @return string */ protected function buildDialDisplay($name, $phone) { $masked = $this->maskPhone($phone); $clean = trim((string)$name); if ($clean === '') { return $masked; } $surname = mb_substr($clean, 0, 1, 'UTF-8'); return $surname . $masked; } /** * 计算指定用户今日的电话拨打额度状态 * * @param int $userId 用户ID(0表示游客) * @param bool $isMember 是否为有效期内VIP会员 * @return array{total:int,used:int,left:int,unlimited:bool} */ protected function callQuotaState($userId, $isMember) { $normalQuota = max(0, (int)$this->cfg('call.normalDailyQuota', 5)); $vipQuota = max(0, (int)$this->cfg('call.vipDailyQuota', 30)); $total = $isMember ? $vipQuota : $normalQuota; $unlimited = ($total === 0); $used = 0; if ($userId > 0) { $today = date('Y-m-d'); $used = (int)Db::name('fc_call_log') ->where('user_id', $userId) ->where('call_date', $today) ->count(); } return [ 'total' => $total, 'used' => $used, 'left' => $unlimited ? 999 : max(0, $total - $used), 'unlimited' => $unlimited, ]; } /** * M2 获取商家名下的信息列表(支持分页) * GET /api/v1/merchant/:id/posts * * @return void */ public function posts() { $id = (int)$this->routeId(); if ($id <= 0) { $id = (int)$this->input('id', 0) ?: (int)$this->input('userId', 0); } if ($id <= 0) { $this->fail('缺少商家参数', 400); } $userId = $id; $m = Db::name('fc_merchant')->where('id', $id)->find(); if ($m) { $userId = (int)$m['user_id']; } list($page, $size) = $this->pager(10, 30); $res = $this->fetchMerchantPosts($userId, $page, $size); $this->ok($res); } /** * 查询指定商家的发帖列表统一组装 * * @param int $userId 发布者用户ID * @param int $page 页码 * @param int $size 每页数量 * @return array */ protected function fetchMerchantPosts($userId, $page, $size) { $q = Db::name('fc_post') ->where('user_id', $userId) ->where('status', 1) ->where('audit_status', 1) ->whereNull('deletetime'); $total = (int)$q->count(); $rows = Db::name('fc_post') ->where('user_id', $userId) ->where('status', 1) ->where('audit_status', 1) ->whereNull('deletetime') ->order('is_top desc, id desc') ->page($page, $size) ->select(); $list = []; foreach ($rows as $r) { $catName = ''; if (!empty($r['category_id'])) { $cat = Db::name('fc_category')->where('id', $r['category_id'])->value('name'); $catName = $cat ? (string)$cat : ''; } $subCatId = isset($r['sub_category_id']) ? (int)$r['sub_category_id'] : 0; $subCatName = ''; if ($subCatId > 0) { $subCat = Db::name('fc_category')->where('id', $subCatId)->value('name'); $subCatName = $subCat ? (string)$subCat : ''; } $cover = (string)$r['cover']; $mediaArr = $this->jsonArr($r['media']); if ($cover === '' && !empty($mediaArr)) { $cover = $mediaArr[0]; } $tags = []; if (!empty($r['tags'])) { foreach (explode(',', (string)$r['tags']) as $t) { $t = trim($t); if ($t !== '') { $tags[] = $t; } } } $list[] = [ 'id' => (int)$r['id'], 'title' => (string)$r['title'], 'content' => (string)$r['content'], 'price' => (float)$r['price'], 'priceUnit' => (string)$r['price_unit'], 'cover' => $cover, 'media' => $mediaArr, 'province' => isset($r['province']) ? (string)$r['province'] : '', 'city' => isset($r['city']) ? (string)$r['city'] : '', 'area' => (string)$r['area'], 'address' => (string)$r['address'], 'plantArea' => isset($r['plant_area']) ? (string)$r['plant_area'] : '', 'harvestTime' => isset($r['harvest_time']) ? (string)$r['harvest_time'] : '', 'outputRange' => isset($r['output_range']) ? (string)$r['output_range'] : '', 'categoryId' => (int)$r['category_id'], 'categoryName' => $catName, 'subCategoryId' => $subCatId, 'subCategoryName' => $subCatName, 'isTop' => (int)$r['is_top'] === 1, 'views' => (int)$r['views'], 'commentCount' => (int)$r['comment_count'], 'createdAt' => $this->iso($r['createtime']), 'tags' => $tags, ]; } return [ 'list' => $list, 'total' => $total, 'page' => $page, 'size' => $size, 'hasMore' => ($page * $size) < $total, ]; } /** * 计算两点大地距离(单位 km,保留 1 位小数) * * @param float $lat1 纬度1 * @param float $lng1 经度1 * @param float $lat2 纬度2 * @param float $lng2 经度2 * @return float */ protected function calcDistance($lat1, $lng1, $lat2, $lng2) { $earthRadius = 6378.137; $radLat1 = deg2rad($lat1); $radLat2 = deg2rad($lat2); $a = $radLat1 - $radLat2; $b = deg2rad($lng1) - deg2rad($lng2); $s = 2 * asin(sqrt(pow(sin($a / 2), 2) + cos($radLat1) * cos($radLat2) * pow(sin($b / 2), 2))); $s = $s * $earthRadius; return round($s, 1); } }