feat: 重构后台服务字典与商品独立定价体系及前后端交互优化
这个提交包含在:
@@ -18,6 +18,118 @@ class Order extends Base
|
||||
$this->success('', ['list' => OrderStatus::all()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 物流单号智能识别物流公司(支持线上快递100识别 + 本地单号特征规则库双保险)
|
||||
*/
|
||||
public function detectExpress()
|
||||
{
|
||||
$raw = strtoupper(trim((string)$this->request->param('express_no', $this->request->param('logistics_no', ''))));
|
||||
$no = preg_replace('/[^A-Z0-9]/', '', $raw);
|
||||
if ($no === '' || strlen($no) < 5) {
|
||||
$this->error('请输入正确的物流单号');
|
||||
}
|
||||
|
||||
$codeMap = [
|
||||
'shunfeng' => '顺丰速运',
|
||||
'debangwuliu' => '德邦快递',
|
||||
'debangkuaidi' => '德邦快递',
|
||||
'jd' => '京东快递',
|
||||
'kuayue' => '跨越速运',
|
||||
'shentong' => '申通快递',
|
||||
'yuantong' => '圆通速递',
|
||||
'zhongtong' => '中通快递',
|
||||
'yunda' => '韵达',
|
||||
'huitongkuaidi' => '百世快递',
|
||||
'baishiwuliu' => '百世快递',
|
||||
'tiantian' => '天天快递',
|
||||
'ems' => '邮政特快专递',
|
||||
'youzhengguonei' => '中国邮政',
|
||||
'youzhengbk' => '中国邮政',
|
||||
'zhaijisong' => '宅急送',
|
||||
'jtexpress' => '极兔速递',
|
||||
'annengwuliu' => '安能物流',
|
||||
];
|
||||
|
||||
$companyName = '';
|
||||
|
||||
// 1. 优先匹配高确定性的标准物流单号前缀规则
|
||||
if (strpos($no, 'SF') === 0) {
|
||||
$companyName = '顺丰速运';
|
||||
} elseif (strpos($no, 'DP') === 0 || strpos($no, 'DB') === 0) {
|
||||
$companyName = '德邦快递';
|
||||
} elseif (strpos($no, 'JD') === 0 || strpos($no, 'KK') === 0) {
|
||||
$companyName = '京东快递';
|
||||
} elseif (strpos($no, 'KY') === 0) {
|
||||
$companyName = '跨越速运';
|
||||
} elseif (strpos($no, 'YT') === 0) {
|
||||
$companyName = '圆通速递';
|
||||
} elseif (strpos($no, 'ST') === 0 || strpos($no, '77') === 0) {
|
||||
$companyName = '申通快递';
|
||||
} elseif (strpos($no, 'ZT') === 0 || preg_match('/^(73|75|76|78)\d{10,12}$/', $no)) {
|
||||
$companyName = '中通快递';
|
||||
} elseif (strpos($no, 'YD') === 0 || preg_match('/^(31|43|46)\d{11,13}$/', $no)) {
|
||||
$companyName = '韵达';
|
||||
} elseif (strpos($no, 'BS') === 0 || strpos($no, 'HT') === 0 || preg_match('/^55\d{11,13}$/', $no)) {
|
||||
$companyName = '百世快递';
|
||||
} elseif (strpos($no, 'TT') === 0 || preg_match('/^66\d{10,12}$/', $no)) {
|
||||
$companyName = '天天快递';
|
||||
} elseif (preg_match('/^(EMS|EA|EB|EC|ED|EE|EF|10|11)\d{7,11}/', $no)) {
|
||||
$companyName = '邮政特快专递';
|
||||
} elseif (preg_match('/^(98|99|YZ)\d{9,13}$/', $no)) {
|
||||
$companyName = '中国邮政';
|
||||
} elseif (strpos($no, 'ZJS') === 0) {
|
||||
$companyName = '宅急送';
|
||||
} elseif (strpos($no, 'JT') === 0) {
|
||||
$companyName = '极兔速递';
|
||||
} elseif (strpos($no, 'ANE') === 0) {
|
||||
$companyName = '安能物流';
|
||||
}
|
||||
|
||||
// 2. 若前缀未命中,尝试快递100智能单号识别接口(1.5秒超时保护)
|
||||
if ($companyName === '') {
|
||||
try {
|
||||
$ctx = stream_context_create([
|
||||
'http' => ['timeout' => 1.5, 'ignore_errors' => true],
|
||||
'ssl' => ['verify_peer' => false, 'verify_peer_name' => false],
|
||||
]);
|
||||
$resp = @file_get_contents('https://www.kuaidi100.com/autonumber/autoComNum?text=' . urlencode($no), false, $ctx);
|
||||
if ($resp) {
|
||||
$json = json_decode($resp, true);
|
||||
if (!empty($json['auto']) && is_array($json['auto'])) {
|
||||
foreach ($json['auto'] as $item) {
|
||||
$code = isset($item['comCode']) ? strtolower((string)$item['comCode']) : '';
|
||||
if (isset($codeMap[$code])) {
|
||||
$companyName = $codeMap[$code];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {}
|
||||
}
|
||||
|
||||
// 3. 常见纯数字位数兜底识别
|
||||
if ($companyName === '') {
|
||||
if (preg_match('/^\d{15}$/', $no)) {
|
||||
$companyName = '德邦快递';
|
||||
} elseif (preg_match('/^\d{12}$/', $no)) {
|
||||
$companyName = '顺丰速运';
|
||||
} elseif (preg_match('/^\d{13}$/', $no)) {
|
||||
$companyName = '圆通速递';
|
||||
} elseif (preg_match('/^\d{14}$/', $no)) {
|
||||
$companyName = '中通快递';
|
||||
} else {
|
||||
$companyName = '德邦快递';
|
||||
}
|
||||
}
|
||||
|
||||
$this->success('已识别物流公司', [
|
||||
'express_no' => $no,
|
||||
'company_name' => $companyName,
|
||||
'matched' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下单(创建待支付订单)
|
||||
* @ApiMethod (POST)
|
||||
@@ -203,6 +315,15 @@ class Order extends Base
|
||||
$goodsImagesPayload = json_encode($extraPayload, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
$orderContact = trim((string)$this->request->post('order_contact', ''));
|
||||
$expressNo = trim((string)$this->request->post('express_no', $this->request->post('logistics_no', '')));
|
||||
$expressCompany = trim((string)$this->request->post('express_company', $this->request->post('logistics_company', '')));
|
||||
$goodsArrived = (int)$this->request->post('goods_arrived', 1);
|
||||
$orderSource = trim((string)$this->request->post('order_source', ''));
|
||||
$sourceOrderNo = trim((string)$this->request->post('source_order_no', ''));
|
||||
$qualReq = trim((string)$this->request->post('qual_req', ''));
|
||||
$masterReq = trim((string)$this->request->post('master_req', ''));
|
||||
|
||||
$id = Db::name('order')->insertGetId([
|
||||
'order_no' => $orderNo,
|
||||
'user_id' => $this->uid(),
|
||||
@@ -227,6 +348,14 @@ class Order extends Base
|
||||
'address' => $address['address'],
|
||||
'lng' => $address['lng'],
|
||||
'lat' => $address['lat'],
|
||||
'order_contact' => mb_substr($orderContact, 0, 100),
|
||||
'express_no' => mb_substr($expressNo, 0, 64),
|
||||
'express_company' => mb_substr($expressCompany, 0, 64),
|
||||
'goods_arrived' => $goodsArrived ? 1 : 0,
|
||||
'order_source' => mb_substr($orderSource, 0, 100),
|
||||
'source_order_no' => mb_substr($sourceOrderNo, 0, 64),
|
||||
'qual_req' => mb_substr($qualReq, 0, 255),
|
||||
'master_req' => mb_substr($masterReq, 0, 255),
|
||||
'appoint_time' => $appointTime ?: null,
|
||||
'base_amount' => $amount['base_amount'],
|
||||
'floor_amount' => $amount['floor_amount'],
|
||||
@@ -510,6 +639,93 @@ class Order extends Base
|
||||
$this->success('订单已删除');
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键晒单 / 取消晒单(仅限已完成订单,控制是否在首页服务动态公开展示)
|
||||
* @ApiMethod (POST)
|
||||
*/
|
||||
public function showcase()
|
||||
{
|
||||
$orderId = (int)$this->request->post('order_id');
|
||||
$isShowcase = (int)$this->request->post('is_showcase', 1) ? 1 : 0;
|
||||
$content = trim((string)$this->request->post('content', ''));
|
||||
|
||||
$order = $this->ownOrder($orderId, 'user');
|
||||
if ((int)$order['status'] !== OrderStatus::COMPLETED) {
|
||||
$this->error('仅已完成的订单支持晒单操作');
|
||||
}
|
||||
|
||||
$now = time();
|
||||
Db::startTrans();
|
||||
try {
|
||||
if ($isShowcase === 1) {
|
||||
$existingReview = Db::name('review')->where('order_id', $orderId)->find();
|
||||
if ($existingReview) {
|
||||
$updateRev = ['is_showcase' => 1];
|
||||
if ($content !== '') {
|
||||
$updateRev['content'] = mb_substr($content, 0, 500);
|
||||
}
|
||||
Db::name('review')->where('id', (int)$existingReview['id'])->update($updateRev);
|
||||
} else {
|
||||
// 若尚未评价直接点击一键晒单,自动复用完工验收图生成五星好评晒单
|
||||
$mediaUrls = Db::name('order_media')
|
||||
->where('order_id', $orderId)
|
||||
->order('id desc')
|
||||
->limit(6)
|
||||
->column('url');
|
||||
$imagesStr = !empty($mediaUrls) ? json_encode(array_values($mediaUrls), JSON_UNESCAPED_UNICODE) : '';
|
||||
$defaultContent = $content !== '' ? mb_substr($content, 0, 500) : '师傅准时上门,施工专业细致,验收非常满意,主动晒单推荐!';
|
||||
Db::name('review')->insert([
|
||||
'order_id' => $orderId,
|
||||
'user_id' => $this->uid(),
|
||||
'master_id' => (int)$order['master_id'],
|
||||
'score' => 5,
|
||||
'tags' => '准时,专业,值得推荐',
|
||||
'content' => $defaultContent,
|
||||
'images' => $imagesStr,
|
||||
'is_anonymous' => 0,
|
||||
'is_showcase' => 1,
|
||||
'status' => 'normal',
|
||||
'createtime' => $now,
|
||||
]);
|
||||
$masterId = (int)$order['master_id'];
|
||||
if ($masterId > 0) {
|
||||
$avg = Db::name('review')->where('master_id', $masterId)->where('status', 'normal')->avg('score');
|
||||
Db::name('master')->where('id', $masterId)->update([
|
||||
'score' => round((float)$avg, 2),
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Db::name('order')->where('id', $orderId)->update([
|
||||
'is_review' => 1,
|
||||
'is_showcase' => 1,
|
||||
'showcase_time' => $now,
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
$showcaseId = $this->syncOrderShowcase($order, 1);
|
||||
} else {
|
||||
Db::name('order')->where('id', $orderId)->update([
|
||||
'is_showcase' => 0,
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
Db::name('review')->where('order_id', $orderId)->update([
|
||||
'is_showcase' => 0,
|
||||
]);
|
||||
$showcaseId = $this->syncOrderShowcase($order, 0);
|
||||
}
|
||||
Db::commit();
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
$this->error('操作失败:' . $e->getMessage());
|
||||
}
|
||||
|
||||
$this->success($isShowcase ? '已公开晒单至首页服务动态' : '已取消公开晒单', [
|
||||
'is_showcase' => $isShowcase,
|
||||
'showcase_id' => isset($showcaseId) ? (int)$showcaseId : 0,
|
||||
'is_review' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单详情(含状态轨迹与师傅信息)
|
||||
*/
|
||||
@@ -600,7 +816,8 @@ class Order extends Base
|
||||
if (empty($goodsList)) {
|
||||
$goodsList = [
|
||||
[
|
||||
'goods_name' => $formattedOrder['goods_name'] ?: $formattedOrder['service_name'],
|
||||
// fa_order 表无 goods_name 字段,回退使用 service_name
|
||||
'goods_name' => $formattedOrder['service_name'],
|
||||
'goods_model' => $formattedOrder['goods_model'] ?: '',
|
||||
'goods_num' => max(1, (int)$formattedOrder['goods_num']),
|
||||
'goods_image' => $formattedOrder['goods_image_url'],
|
||||
@@ -610,6 +827,39 @@ class Order extends Base
|
||||
];
|
||||
}
|
||||
|
||||
$review = null;
|
||||
$revRow = Db::name('review')->where('order_id', $orderId)->where('status', 'normal')->find();
|
||||
if ($revRow) {
|
||||
$revTags = [];
|
||||
if (!empty($revRow['tags'])) {
|
||||
$decTags = json_decode($revRow['tags'], true);
|
||||
$revTags = is_array($decTags) ? $decTags : array_values(array_filter(explode(',', (string)$revRow['tags'])));
|
||||
}
|
||||
$revImgs = [];
|
||||
if (!empty($revRow['images'])) {
|
||||
$decImgs = json_decode($revRow['images'], true);
|
||||
$rawImgs = is_array($decImgs) ? $decImgs : array_values(array_filter(explode(',', (string)$revRow['images'])));
|
||||
foreach ($rawImgs as $rImg) {
|
||||
if ($rImg) {
|
||||
$revImgs[] = cdnurl(trim($rImg), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
$review = [
|
||||
'id' => (int)$revRow['id'],
|
||||
'score' => (int)$revRow['score'],
|
||||
'tags' => $revTags,
|
||||
'content' => (string)$revRow['content'],
|
||||
'images' => $revImgs,
|
||||
'is_anonymous' => (int)$revRow['is_anonymous'],
|
||||
'is_showcase' => isset($revRow['is_showcase']) ? (int)$revRow['is_showcase'] : (int)$formattedOrder['is_showcase'],
|
||||
'time_text' => date('Y-m-d H:i', (int)$revRow['createtime']),
|
||||
];
|
||||
}
|
||||
|
||||
$showcaseRow = Db::name('showcase')->where('order_id', $orderId)->where('status', 'normal')->find();
|
||||
$formattedOrder['showcase_id'] = $showcaseRow ? (int)$showcaseRow['id'] : 0;
|
||||
|
||||
$this->success('', [
|
||||
'order' => $formattedOrder,
|
||||
'goods_list' => $goodsList,
|
||||
@@ -617,6 +867,7 @@ class Order extends Base
|
||||
'logs' => $logs,
|
||||
'media' => $media,
|
||||
'quotes' => $quotes,
|
||||
'review' => $review,
|
||||
'can_choose_quote' => $order['price_type'] === 'quote'
|
||||
&& (int)$order['status'] === OrderStatus::QUOTING
|
||||
&& (int)$order['master_id'] === 0,
|
||||
@@ -941,6 +1192,8 @@ class Order extends Base
|
||||
'can_delete' => in_array($status, [OrderStatus::CANCELLED, OrderStatus::COMPLETED], true),
|
||||
'can_verify' => $status === OrderStatus::PENDING_VERIFY,
|
||||
'can_review' => $status === OrderStatus::COMPLETED && (int)$row['is_review'] === 0,
|
||||
'can_showcase' => $status === OrderStatus::COMPLETED,
|
||||
'is_showcase' => isset($row['is_showcase']) ? (int)$row['is_showcase'] : 0,
|
||||
'can_pay' => $status === OrderStatus::PENDING_PAY,
|
||||
'pay_amount' => (float)$row['pay_amount'],
|
||||
'total_amount' => (float)$row['total_amount'],
|
||||
|
||||
在新工单中引用
屏蔽一个用户