feat(front): 新增企业订单列表页面及相关功能

- 新增订单列表页面UI,支持状态筛选和搜索功能
- 新增订单删除API接口,支持用户软删除已完成/已取消订单
- 新增订单详情接口返回商品列表和联系信息
- 新增多个图标资源和API接口
- 修改上传配置和订单相关逻辑
这个提交包含在:
YuanWen
2026-11-06 10:40:43 +08:00
父节点 cd0da0324b
当前提交 a3ff0a3d46
共有 32 个文件被更改,包括 6699 次插入 和 1489 次删除

查看文件

@@ -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'],
]
]);
}
}