- 新增订单列表页面UI,支持状态筛选和搜索功能 - 新增订单删除API接口,支持用户软删除已完成/已取消订单 - 新增订单详情接口返回商品列表和联系信息 - 新增多个图标资源和API接口 - 修改上传配置和订单相关逻辑
978 行
39 KiB
PHP
978 行
39 KiB
PHP
<?php
|
||
|
||
namespace app\api\controller;
|
||
|
||
use think\Db;
|
||
|
||
/**
|
||
* 首页 / 服务类目 / 服务项目 / 费用试算
|
||
*/
|
||
class Home extends Base
|
||
{
|
||
protected $noNeedLogin = ['index', 'cats', 'services', 'detail', 'calc', 'specs', 'masterCount', 'orderCase'];
|
||
|
||
/**
|
||
* 首页聚合:类目 + 热门服务 + 我的角标
|
||
*/
|
||
public function index()
|
||
{
|
||
$rawCats = Db::name('service_cat')
|
||
->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'])) {
|
||
$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);
|
||
}
|
||
|
||
/**
|
||
* 全部类目
|
||
*/
|
||
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;
|
||
} 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' => 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) {
|
||
if ((int)$c['id'] === 15 || $c['name'] === '其它' || $c['name'] === '其他') {
|
||
$c['name'] = '全部分类';
|
||
}
|
||
$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 ($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'],
|
||
]
|
||
]);
|
||
}
|
||
}
|