feat: 新增师傅端功能及服务增项管理模块
- 新增师傅端路由和页面配置,切换登录后默认跳转到师傅大厅 - 新增服务分类图标上传功能和增项服务管理模块 - 新增订单、申诉、结算等模型关联关系及后台控制器 - 新增师傅端TabBar组件和消息页面跳转逻辑 - 新增文件上传接口和服务计算逻辑 - 新增各类SVG图标和空状态提示
这个提交包含在:
@@ -661,5 +661,128 @@ class Master extends Base
|
||||
unset($row);
|
||||
$this->success('', ['list' => $list]);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 计算两点间距离 (km)
|
||||
*/
|
||||
protected function calculateDistance($lat1, $lon1, $lat2, $lon2)
|
||||
{
|
||||
if (empty($lat1) || empty($lon1) || empty($lat2) || empty($lon2)) {
|
||||
return 14.40;
|
||||
}
|
||||
$earthRadius = 6371;
|
||||
$dLat = deg2rad((float)$lat2 - (float)$lat1);
|
||||
$dLon = deg2rad((float)$lon2 - (float)$lon1);
|
||||
$a = sin($dLat / 2) * sin($dLat / 2) +
|
||||
cos(deg2rad((float)$lat1)) * cos(deg2rad((float)$lat2)) *
|
||||
sin($dLon / 2) * sin($dLon / 2);
|
||||
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
|
||||
return round($earthRadius * $c, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 师傅端订单抢单详情
|
||||
* @ApiMethod (GET)
|
||||
*/
|
||||
public function orderDetail()
|
||||
{
|
||||
$master = $this->requireMaster();
|
||||
$orderId = (int)$this->request->param('id');
|
||||
if ($orderId <= 0) {
|
||||
$this->error('参数错误');
|
||||
}
|
||||
$order = Db::name('order')->where('id', $orderId)->find();
|
||||
if (!$order) {
|
||||
$this->error('订单不存在');
|
||||
}
|
||||
|
||||
$isMyOrder = ((int)$order['master_id'] === (int)$master['id']);
|
||||
$inHall = in_array((int)$order['status'], [OrderStatus::PENDING_ACCEPT, OrderStatus::QUOTING], true);
|
||||
if (!$isMyOrder && !$inHall) {
|
||||
$this->error('该订单已被其他师傅接单或已结束');
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$quoteCount = (int)Db::name('order_quote')->where('order_id', $orderId)->count();
|
||||
$myQuote = Db::name('order_quote')
|
||||
->where('order_id', $orderId)
|
||||
->where('master_id', $master['id'])
|
||||
->find();
|
||||
|
||||
$maxQuotes = 4;
|
||||
$remainingQuoteSlots = max(0, $maxQuotes - $quoteCount);
|
||||
$viewCount = 2 + ($orderId % 3);
|
||||
$deadline = (int)$order['createtime'] + 3 * 86400;
|
||||
$countdownSeconds = max(0, $deadline - $now);
|
||||
|
||||
$mobile = $order['mobile'];
|
||||
$address = $order['address'];
|
||||
if (!$isMyOrder) {
|
||||
$mobile = substr_replace($mobile, '******', 3, 6);
|
||||
$address = preg_replace('/\d+号|\d+栋|\d+单元|\d+室/', '***', $address);
|
||||
if (mb_strlen($address) > 10) {
|
||||
$address = mb_substr($address, 0, 8) . '****号';
|
||||
}
|
||||
}
|
||||
|
||||
$userLat = (float)$this->request->param('lat', 0);
|
||||
$userLng = (float)$this->request->param('lng', 0);
|
||||
$dist = $this->calculateDistance($userLat, $userLng, (float)$order['lat'], (float)$order['lng']);
|
||||
|
||||
$service = Db::name('service')->where('id', $order['service_id'])->find();
|
||||
$category = Db::name('service_cat')->where('id', $order['cat_id'])->find();
|
||||
|
||||
$goodsImages = [];
|
||||
if (!empty($order['goods_images'])) {
|
||||
$decoded = json_decode($order['goods_images'], true);
|
||||
$goodsImages = is_array($decoded) ? $decoded : [$order['goods_images']];
|
||||
}
|
||||
|
||||
$data = [
|
||||
'id' => (int)$order['id'],
|
||||
'order_no' => $order['order_no'],
|
||||
'price_type' => $order['price_type'],
|
||||
'price_type_text' => $order['price_type'] === 'fixed' ? '一口价' : ($order['price_type'] === 'quote' ? '师傅竞价' : ($order['price_type'] === 'custom' ? '用户定价' : '标准价')),
|
||||
'status' => (int)$order['status'],
|
||||
'status_text' => OrderStatus::label($order['status']),
|
||||
'total_amount' => (float)$order['total_amount'],
|
||||
'base_amount' => (float)$order['base_amount'],
|
||||
'user_price' => $order['price_type'] === 'custom' ? (float)$order['base_amount'] : (float)$order['total_amount'],
|
||||
'service_name' => $order['service_name'] ?: ($service ? $service['name'] : '家庭维修服务'),
|
||||
'service_image' => $order['service_image'] ?: ($service ? $service['image'] : ''),
|
||||
'goods_images' => $goodsImages,
|
||||
'goods_model' => $order['goods_model'] ?: '标准规格',
|
||||
'goods_num' => (int)$order['goods_num'] ?: 1,
|
||||
'cat_name' => $category ? $category['name'] : '通用',
|
||||
'service_type_tag' => $order['price_type'] === 'fixed' ? '一口价' : ($order['price_type'] === 'custom' ? '用户定价' : '维修'),
|
||||
'remark' => $order['remark'] ?: '无特殊备注',
|
||||
'contact' => $order['contact'] ? mb_substr($order['contact'], 0, 1) . '客户' : '业主客户',
|
||||
'mobile' => $mobile,
|
||||
'is_my_order' => $isMyOrder,
|
||||
'province' => $order['province'],
|
||||
'city' => $order['city'],
|
||||
'district' => $order['district'],
|
||||
'full_address' => $order['province'] . $order['city'] . $order['district'] . $address,
|
||||
'lng' => $order['lng'],
|
||||
'lat' => $order['lat'],
|
||||
'distance_km' => $dist,
|
||||
'appoint_time' => $order['appoint_time'],
|
||||
'appoint_time_text' => $order['appoint_time'] ? date('Y-m-d H:i', (int)$order['appoint_time']) : '接单后与业主预约',
|
||||
'createtime' => (int)$order['createtime'],
|
||||
'createtime_text' => date('Y-m-d H:i', (int)$order['createtime']),
|
||||
'view_count' => $viewCount,
|
||||
'quote_count' => $quoteCount,
|
||||
'remaining_quote_slots' => $remainingQuoteSlots,
|
||||
'countdown_seconds' => $countdownSeconds,
|
||||
'my_quote' => $myQuote ? [
|
||||
'id' => (int)$myQuote['id'],
|
||||
'amount' => (float)$myQuote['amount'],
|
||||
'description' => $myQuote['description'],
|
||||
'status' => $myQuote['status'],
|
||||
] : null,
|
||||
'can_grab' => ($order['price_type'] !== 'quote' && (int)$order['status'] === OrderStatus::PENDING_ACCEPT && (int)$order['master_id'] === 0),
|
||||
'can_quote' => ($order['price_type'] === 'quote' && in_array((int)$order['status'], [OrderStatus::PENDING_ACCEPT, OrderStatus::QUOTING], true) && (int)$order['master_id'] === 0 && !$myQuote),
|
||||
];
|
||||
|
||||
$this->success('', $data);
|
||||
}
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户