feat(front): 新增企业订单列表页面及相关功能
- 新增订单列表页面UI,支持状态筛选和搜索功能 - 新增订单删除API接口,支持用户软删除已完成/已取消订单 - 新增订单详情接口返回商品列表和联系信息 - 新增多个图标资源和API接口 - 修改上传配置和订单相关逻辑
这个提交包含在:
@@ -41,10 +41,24 @@ class Order extends Base
|
||||
$goodsImages = $this->request->post('goods_images', '');
|
||||
|
||||
// 支持多商品组合 (goods_list)
|
||||
$goodsListRaw = $this->request->post('goods_list');
|
||||
$goodsListRaw = $this->request->post('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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$service = null;
|
||||
@@ -341,18 +355,40 @@ class Order extends Base
|
||||
public function index()
|
||||
{
|
||||
$status = $this->request->param('status', 'all');
|
||||
$keyword = trim((string)$this->request->param('keyword', ''));
|
||||
$page = max(1, (int)$this->request->param('page', 1));
|
||||
$limit = min(50, max(1, (int)$this->request->param('limit', 10)));
|
||||
|
||||
// 注意:TP5 中 count() 执行后查询条件会被重置,必须重新构建查询
|
||||
$build = function () use ($status) {
|
||||
$q = Db::name('order')->where('user_id', $this->uid());
|
||||
$build = function () use ($status, $keyword) {
|
||||
$q = Db::name('order')->where('user_id', $this->uid())->where('user_deleted', 0);
|
||||
if ($keyword !== '') {
|
||||
$q->where(function ($sub) use ($keyword) {
|
||||
$sub->where('contact', 'like', "%{$keyword}%")
|
||||
->whereOr('mobile', 'like', "%{$keyword}%")
|
||||
->whereOr('order_no', 'like', "%{$keyword}%")
|
||||
->whereOr('service_name', 'like', "%{$keyword}%")
|
||||
->whereOr('address', 'like', "%{$keyword}%")
|
||||
->whereOr('remark', 'like', "%{$keyword}%");
|
||||
});
|
||||
}
|
||||
if ($status === 'active') {
|
||||
$q->where('status', 'in', OrderStatus::activeValues());
|
||||
} elseif ($status === 'completed') {
|
||||
} elseif ($status === 'completed' || $status === '8') {
|
||||
$q->where('status', OrderStatus::COMPLETED);
|
||||
} elseif ($status === 'cancelled') {
|
||||
} elseif ($status === 'cancelled' || $status === '9') {
|
||||
$q->where('status', OrderStatus::CANCELLED);
|
||||
} elseif ($status === 'pending_pay' || $status === '0') {
|
||||
$q->where('status', OrderStatus::PENDING_PAY);
|
||||
} elseif ($status === 'pending_accept' || $status === '1') {
|
||||
$q->where('status', OrderStatus::PENDING_ACCEPT);
|
||||
} elseif ($status === 'quoting' || $status === '2') {
|
||||
$q->where('status', OrderStatus::QUOTING);
|
||||
} elseif ($status === 'scheduled' || $status === '3') {
|
||||
$q->where('status', OrderStatus::ACCEPTED);
|
||||
} elseif ($status === 'serving' || $status === '4_6') {
|
||||
$q->where('status', 'in', [OrderStatus::SCHEDULED, OrderStatus::ARRIVED, OrderStatus::WORKING]);
|
||||
} elseif ($status === 'pending_verify' || $status === '7') {
|
||||
$q->where('status', OrderStatus::PENDING_VERIFY);
|
||||
} elseif ($status !== 'all' && $status !== '') {
|
||||
$q->where('status', (int)$status);
|
||||
}
|
||||
@@ -361,14 +397,50 @@ class Order extends Base
|
||||
|
||||
$total = $build()->count();
|
||||
$list = $build()->order('id desc')->page($page, $limit)->select();
|
||||
|
||||
$orderIds = array_column($list, 'id');
|
||||
$allGoods = [];
|
||||
if (!empty($orderIds)) {
|
||||
$goodsRows = Db::name('order_goods')->where('order_id', 'in', $orderIds)->select();
|
||||
foreach ($goodsRows as $gr) {
|
||||
$allGoods[$gr['order_id']][] = $gr;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($list as &$row) {
|
||||
$row = $this->formatOrder($row);
|
||||
if (!empty($allGoods[$row['id']])) {
|
||||
$row['goods_list'] = $allGoods[$row['id']];
|
||||
$firstG = $allGoods[$row['id']][0];
|
||||
$row['goods_title'] = $firstG['goods_name'] . (!empty($firstG['goods_model']) ? ' (' . $firstG['goods_model'] . ')' : '') . ' x ' . $firstG['goods_num'] . '套';
|
||||
if (!empty($firstG['addon_services'])) {
|
||||
$row['service_items_text'] = '服务项目: ' . $firstG['addon_services'];
|
||||
}
|
||||
if (!empty($firstG['goods_image'])) {
|
||||
$row['goods_image_url'] = $firstG['goods_image'];
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($row);
|
||||
|
||||
$this->success('', ['list' => $list, 'total' => (int)$total, 'page' => $page]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除订单(用户端软删除,隐藏订单)
|
||||
* @ApiMethod (POST)
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
$orderId = (int)$this->request->post('order_id');
|
||||
$order = $this->ownOrder($orderId, 'user');
|
||||
if (!in_array((int)$order['status'], [OrderStatus::CANCELLED, OrderStatus::COMPLETED], true)) {
|
||||
$this->error('仅已关闭或已完成的订单支持删除');
|
||||
}
|
||||
Db::name('order')->where('id', $orderId)->update(['user_deleted' => 1, 'updatetime' => time()]);
|
||||
$this->success('订单已删除');
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单详情(含状态轨迹与师傅信息)
|
||||
*/
|
||||
@@ -449,9 +521,29 @@ class Order extends Base
|
||||
}
|
||||
}
|
||||
|
||||
$creatorUser = Db::name('user')->where('id', (int)$order['user_id'])->find();
|
||||
$orderContact = $creatorUser ? (($creatorUser['username'] ?: '用户') . ', ' . ($creatorUser['mobile'] ?: $order['mobile'])) : $order['mobile'];
|
||||
$formattedOrder = $this->formatOrder($order);
|
||||
$formattedOrder['order_contact'] = $orderContact;
|
||||
$formattedOrder['remark_display'] = !empty($order['remark']) ? $order['remark'] : '无备注';
|
||||
|
||||
$goodsList = Db::name('order_goods')->where('order_id', $orderId)->select();
|
||||
if (empty($goodsList)) {
|
||||
$goodsList = [
|
||||
[
|
||||
'goods_name' => $formattedOrder['goods_name'] ?: $formattedOrder['service_name'],
|
||||
'goods_model' => $formattedOrder['goods_model'] ?: '',
|
||||
'goods_num' => max(1, (int)$formattedOrder['goods_num']),
|
||||
'goods_image' => $formattedOrder['goods_image_url'],
|
||||
'addon_services' => '安装x' . max(1, (int)$formattedOrder['goods_num']) . ';',
|
||||
'base_price' => (float)$formattedOrder['pay_amount'],
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
$this->success('', [
|
||||
'order' => $this->formatOrder($order),
|
||||
'goods_list' => Db::name('order_goods')->where('order_id', $orderId)->select(),
|
||||
'order' => $formattedOrder,
|
||||
'goods_list' => $goodsList,
|
||||
'master' => $master,
|
||||
'logs' => $logs,
|
||||
'media' => $media,
|
||||
@@ -501,11 +593,19 @@ class Order extends Base
|
||||
|
||||
$quoteAmount = round((float)$quote['amount'], 2);
|
||||
$payAmount = max(0, round($quoteAmount - (float)$order['discount_amount'], 2));
|
||||
$remark = '客户已选择 ' . $master['realname'] . ' 的报价 ¥' . $quoteAmount . ',等待师傅联系';
|
||||
$alreadyPaid = (int)$order['pay_status'] === 1 ? (float)$order['pay_amount'] : 0.0;
|
||||
$needDiff = max(0, round($payAmount - $alreadyPaid, 2));
|
||||
|
||||
$payType = $this->request->post('pay_type', 'wechat');
|
||||
$remark = '客户已选择 ' . $master['realname'] . ' 的报价 ¥' . $quoteAmount;
|
||||
if ($needDiff > 0) {
|
||||
$remark .= '(补付差额 ¥' . $needDiff . ')';
|
||||
}
|
||||
$remark .= ',等待师傅联系';
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
$this->transit($order, OrderStatus::ACCEPTED, $remark, [
|
||||
$updateData = [
|
||||
'master_id' => (int)$quote['master_id'],
|
||||
'accept_time' => time(),
|
||||
'base_amount' => $quoteAmount,
|
||||
@@ -513,7 +613,13 @@ class Order extends Base
|
||||
'urgent_amount' => 0,
|
||||
'total_amount' => $quoteAmount,
|
||||
'pay_amount' => $payAmount,
|
||||
], 'user', $this->uid());
|
||||
'pay_status' => 1,
|
||||
];
|
||||
if ($needDiff > 0) {
|
||||
$updateData['pay_type'] = $payType;
|
||||
$updateData['pay_time'] = time();
|
||||
}
|
||||
$this->transit($order, OrderStatus::ACCEPTED, $remark, $updateData, 'user', $this->uid());
|
||||
|
||||
Db::name('order_quote')->where('id', $quoteId)->update(['status' => 'accept']);
|
||||
Db::name('order_quote')->where('order_id', $orderId)->where('id', '<>', $quoteId)
|
||||
@@ -528,7 +634,11 @@ class Order extends Base
|
||||
$this->error('选择报价失败:' . $e->getMessage());
|
||||
}
|
||||
|
||||
$this->success('已选定师傅,请等待师傅联系');
|
||||
$this->success('已选定师傅并完成支付托管,请等待师傅联系', [
|
||||
'pay_amount' => $payAmount,
|
||||
'need_diff' => $needDiff,
|
||||
'already_pay' => $alreadyPaid,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -689,28 +799,101 @@ class Order extends Base
|
||||
{
|
||||
$status = (int)$row['status'];
|
||||
$appoint = (int)$row['appoint_time'];
|
||||
$mobile = (string)$row['mobile'];
|
||||
$mobileMask = strlen($mobile) === 11 ? substr_replace($mobile, '****', 3, 4) : $mobile;
|
||||
|
||||
$prov = isset($row['province']) ? $row['province'] : '';
|
||||
$city = isset($row['city']) ? $row['city'] : '';
|
||||
$dist = isset($row['district']) ? $row['district'] : '';
|
||||
$addr = isset($row['address']) ? $row['address'] : '';
|
||||
$fullAddress = trim($prov . $city . $dist . $addr);
|
||||
if (empty($fullAddress)) {
|
||||
$fullAddress = $addr;
|
||||
}
|
||||
|
||||
// 状态轨迹提示文案与时间
|
||||
$statusTimelineText = OrderStatus::tip($status);
|
||||
$statusTime = !empty($row['updatetime']) ? (int)$row['updatetime'] : (int)$row['createtime'];
|
||||
if ($status === OrderStatus::CANCELLED) {
|
||||
$statusTimelineText = ((int)$row['pay_status'] === 0) ? '订单未支付,您已取消订单' : '订单已取消,退款处理中';
|
||||
} elseif ($status === OrderStatus::PENDING_PAY) {
|
||||
$statusTimelineText = '订单待支付,请尽快完成支付';
|
||||
} elseif ($status === OrderStatus::PENDING_ACCEPT) {
|
||||
$statusTimelineText = '订单已进入接单大厅,正在匹配师傅';
|
||||
} elseif ($status === OrderStatus::QUOTING) {
|
||||
$statusTimelineText = '已有师傅进行报价,请比对选择';
|
||||
} elseif ($status === OrderStatus::ACCEPTED) {
|
||||
$statusTimelineText = '师傅已接单,请等待师傅联系并上门';
|
||||
if (!empty($row['accept_time'])) $statusTime = (int)$row['accept_time'];
|
||||
} elseif ($status === OrderStatus::SCHEDULED) {
|
||||
$statusTimelineText = '已预约上门服务时间:' . ($appoint ? date('Y-m-d H:i', $appoint) : '尽快上门');
|
||||
} elseif ($status === OrderStatus::ARRIVED) {
|
||||
$statusTimelineText = '师傅已到达现场,准备开工';
|
||||
if (!empty($row['arrive_time'])) $statusTime = (int)$row['arrive_time'];
|
||||
} elseif ($status === OrderStatus::WORKING) {
|
||||
$statusTimelineText = '师傅施工中,请稍候';
|
||||
if (!empty($row['start_time'])) $statusTime = (int)$row['start_time'];
|
||||
} elseif ($status === OrderStatus::PENDING_VERIFY) {
|
||||
$statusTimelineText = '师傅已完工,请核验施工质量后验收';
|
||||
if (!empty($row['finish_time'])) $statusTime = (int)$row['finish_time'];
|
||||
} elseif ($status === OrderStatus::COMPLETED) {
|
||||
$statusTimelineText = '服务已完成,期待您的评价';
|
||||
if (!empty($row['verify_time'])) $statusTime = (int)$row['verify_time'];
|
||||
}
|
||||
|
||||
// 期望上门文本
|
||||
$appointDateText = '尽快上门';
|
||||
if ($appoint > 0) {
|
||||
$hour = (int)date('H', $appoint);
|
||||
$period = $hour < 12 ? '上午' : ($hour < 18 ? '下午' : '晚上');
|
||||
$appointDateText = date('Y-n-j', $appoint) . ' ' . $period;
|
||||
}
|
||||
|
||||
// 商品与服务概览
|
||||
$goodsNum = max(1, (int)$row['goods_num']);
|
||||
$goodsTitle = ($row['service_name'] ?: '专业师傅上门服务') . ' x ' . $goodsNum . '套';
|
||||
$serviceItemsText = '服务项目: 安装x' . $goodsNum . ';';
|
||||
if (!empty($row['goods_model'])) {
|
||||
$serviceItemsText = '规格型号: ' . $row['goods_model'] . '; 安装x' . $goodsNum . ';';
|
||||
}
|
||||
|
||||
// 默认商品图/服务图
|
||||
$goodsImageUrl = !empty($row['service_image']) ? $row['service_image'] : '/static/icons/appliance.png';
|
||||
|
||||
return array_merge($row, [
|
||||
'id' => (int)$row['id'],
|
||||
'status' => $status,
|
||||
'status_text' => OrderStatus::label($status),
|
||||
'status_tip' => OrderStatus::tip($status),
|
||||
'can_cancel' => OrderStatus::canCancelByUser($status),
|
||||
'can_verify' => $status === OrderStatus::PENDING_VERIFY,
|
||||
'can_review' => $status === OrderStatus::COMPLETED && (int)$row['is_review'] === 0,
|
||||
'can_pay' => $status === OrderStatus::PENDING_PAY,
|
||||
'pay_amount' => (float)$row['pay_amount'],
|
||||
'total_amount' => (float)$row['total_amount'],
|
||||
'appoint_text' => $appoint ? date('m-d H:i', $appoint) : '尽快上门',
|
||||
'create_text' => date('Y-m-d H:i', (int)$row['createtime']),
|
||||
'urgent' => (int)$row['urgent'],
|
||||
'price_type_text' => $this->priceTypeText($row['price_type']),
|
||||
'is_order_owner' => (int)$row['user_id'] === $this->uid(),
|
||||
'diff_amount' => isset($row['diff_amount']) ? (float)$row['diff_amount'] : 0.00,
|
||||
'diff_status' => isset($row['diff_status']) ? (int)$row['diff_status'] : 0,
|
||||
'diff_remark' => isset($row['diff_remark']) ? (string)$row['diff_remark'] : '',
|
||||
'diff_image' => isset($row['diff_image']) ? (string)$row['diff_image'] : '',
|
||||
'empty_run_fee' => isset($row['empty_run_fee']) ? (float)$row['empty_run_fee'] : 0.00,
|
||||
'auto_quote_timeout' => isset($row['auto_quote_timeout']) ? (int)$row['auto_quote_timeout'] : 0,
|
||||
'id' => (int)$row['id'],
|
||||
'status' => $status,
|
||||
'status_text' => OrderStatus::label($status),
|
||||
'status_tip' => OrderStatus::tip($status),
|
||||
'status_timeline_text'=> $statusTimelineText,
|
||||
'status_time_text' => date('Y-m-d H:i:s', $statusTime),
|
||||
'status_time_short' => date('Y/m/d H:i:s', $statusTime),
|
||||
'can_cancel' => OrderStatus::canCancelByUser($status),
|
||||
'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_pay' => $status === OrderStatus::PENDING_PAY,
|
||||
'pay_amount' => (float)$row['pay_amount'],
|
||||
'total_amount' => (float)$row['total_amount'],
|
||||
'appoint_text' => $appoint ? date('m-d H:i', $appoint) : '尽快上门',
|
||||
'appoint_date_text' => $appointDateText,
|
||||
'create_text' => date('Y-m-d H:i', (int)$row['createtime']),
|
||||
'create_full_text' => date('Y/m/d H:i:s', (int)$row['createtime']),
|
||||
'urgent' => (int)$row['urgent'],
|
||||
'price_type_text' => $this->priceTypeText($row['price_type']),
|
||||
'price_type_tag' => $row['price_type'] === 'quote' ? '报价' : ($row['price_type'] === 'custom' ? '自定价' : '一口价'),
|
||||
'is_order_owner' => (int)$row['user_id'] === $this->uid(),
|
||||
'mobile_mask' => $mobileMask,
|
||||
'full_address' => $fullAddress,
|
||||
'goods_title' => $goodsTitle,
|
||||
'service_items_text' => $serviceItemsText,
|
||||
'goods_image_url' => $goodsImageUrl,
|
||||
'diff_amount' => isset($row['diff_amount']) ? (float)$row['diff_amount'] : 0.00,
|
||||
'diff_status' => isset($row['diff_status']) ? (int)$row['diff_status'] : 0,
|
||||
'diff_remark' => isset($row['diff_remark']) ? (string)$row['diff_remark'] : '',
|
||||
'diff_image' => isset($row['diff_image']) ? (string)$row['diff_image'] : '',
|
||||
'empty_run_fee' => isset($row['empty_run_fee']) ? (float)$row['empty_run_fee'] : 0.00,
|
||||
'auto_quote_timeout' => isset($row['auto_quote_timeout']) ? (int)$row['auto_quote_timeout'] : 0,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -753,5 +936,88 @@ class Order extends Base
|
||||
];
|
||||
return isset($map[$type]) ? $map[$type] : '一口价';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 超时未验收订单自动核销(默认48小时无争议自动验收结算,保障师傅权益)
|
||||
*/
|
||||
public static function autoVerifyExpired($timeoutSeconds = 172800)
|
||||
{
|
||||
$expiredTime = time() - (int)$timeoutSeconds;
|
||||
$rows = Db::name('order')
|
||||
->where('status', OrderStatus::PENDING_VERIFY)
|
||||
->where('settle_status', 0)
|
||||
->where('finish_time', '<', $expiredTime)
|
||||
->limit(20)
|
||||
->select();
|
||||
|
||||
$count = 0;
|
||||
foreach ($rows as $order) {
|
||||
$orderId = (int)$order['id'];
|
||||
$masterId = (int)$order['master_id'];
|
||||
$payAmount = (float)$order['pay_amount'];
|
||||
$rate = 10.00;
|
||||
$commission = round($payAmount * $rate / 100, 2);
|
||||
$settleAmount = round($payAmount - $commission, 2);
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
Db::name('order')->where('id', $orderId)->update([
|
||||
'status' => OrderStatus::COMPLETED,
|
||||
'verify_time' => time(),
|
||||
'settle_status' => 1,
|
||||
'settle_amount' => $settleAmount,
|
||||
'updatetime' => time(),
|
||||
]);
|
||||
|
||||
Db::name('order_log')->insert([
|
||||
'order_id' => $orderId,
|
||||
'status' => OrderStatus::COMPLETED,
|
||||
'remark' => '完工超48小时未验收,系统自动确认完成并结算',
|
||||
'operator_type' => 'system',
|
||||
'operator_id' => 0,
|
||||
'createtime' => time(),
|
||||
]);
|
||||
|
||||
Db::name('settle')->insert([
|
||||
'order_id' => $orderId,
|
||||
'master_id' => $masterId,
|
||||
'order_amount' => $payAmount,
|
||||
'commission_rate' => $rate,
|
||||
'commission' => $commission,
|
||||
'settle_amount' => $settleAmount,
|
||||
'status' => 'settled',
|
||||
'settle_time' => time(),
|
||||
'createtime' => time(),
|
||||
]);
|
||||
|
||||
if ($masterId > 0) {
|
||||
$master = Db::name('master')->where('id', $masterId)->lock(true)->find();
|
||||
if ($master) {
|
||||
$newMoney = round((float)$master['money'] + $settleAmount, 2);
|
||||
Db::name('master')->where('id', $masterId)->update([
|
||||
'money' => $newMoney,
|
||||
'order_count' => (int)$master['order_count'] + 1,
|
||||
'updatetime' => time(),
|
||||
]);
|
||||
Db::name('wallet_log')->insert([
|
||||
'user_type' => 'master',
|
||||
'user_id' => $masterId,
|
||||
'type' => 'settle',
|
||||
'amount' => $settleAmount,
|
||||
'balance' => $newMoney,
|
||||
'order_id' => $orderId,
|
||||
'remark' => '工单超时自动验收结算入账',
|
||||
'createtime' => time(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Db::commit();
|
||||
$count++;
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
}
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户