文件
yjyf/server/application/api/controller/Order.php

1346 行
61 KiB
PHP

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<?php
namespace app\api\controller;
use app\common\library\OrderStatus;
use think\Db;
/**
* 用户端订单
*/
class Order extends Base
{
/**
* 状态字典(供前端渲染筛选 Tab)
*/
public function statusList()
{
$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)
*/
public function create()
{
// 频次防刷限制:同一用户 3 秒内禁止连续下单,防并发重复创单
$lastOrderTime = Db::name('order')->where('user_id', $this->uid())->order('id desc')->value('createtime');
if ($lastOrderTime && (time() - (int)$lastOrderTime) < 3) {
$this->error('操作过于频繁,请稍候再试');
}
$serviceId = (int)$this->request->post('service_id');
$addressId = (int)$this->request->post('address_id');
$appointTime = (int)$this->request->post('appoint_time', 0);
$urgent = (int)$this->request->post('urgent', 0);
$couponId = (int)$this->request->post('coupon_id', 0);
$goodsModel = trim((string)$this->request->post('goods_model'));
$goodsNum = max(1, (int)$this->request->post('goods_num', 1));
$remark = trim((string)$this->request->post('remark'));
$goodsImages = $this->request->post('goods_images', '');
// 支持多商品组合 (goods_list)
$goodsListRaw = $this->request->post('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;
}
}
}
$service = null;
if (!empty($goodsList) && is_array($goodsList)) {
$first = reset($goodsList);
$serviceId = isset($first['service_id']) ? (int)$first['service_id'] : $serviceId;
}
if ($serviceId > 0) {
$service = Db::name('service')->where('id', $serviceId)->where('status', 'normal')->find();
}
if (!$service && empty($goodsList)) {
$this->error('服务不存在或已下架');
}
// 下单页可选择:一口价 / 师傅报价 / 用户自主定价(未传则沿用服务默认)
$defaultPriceType = $service ? $service['price_type'] : 'fixed';
$priceType = trim((string)$this->request->post('price_type', $defaultPriceType));
if (!in_array($priceType, ['fixed', 'quote', 'custom', 'range'], true)) {
$priceType = $defaultPriceType;
}
$userPrice = round((float)$this->request->post('user_price', 0), 2);
if ($priceType === 'custom') {
if ($userPrice < 1 || $userPrice > 100000) {
$this->error('请填写 1-100000 元的出价');
}
} else {
$userPrice = 0;
}
// 支持直接传 address_id 或传入新地址对象并自动保存
$address = null;
if ($addressId > 0) {
$address = Db::name('address')->where('id', $addressId)->where('user_id', $this->uid())->find();
}
if (!$address) {
$contact = trim((string)$this->request->post('contact'));
$mobile = trim((string)$this->request->post('mobile'));
$province = trim((string)$this->request->post('province', '贵州省'));
$city = trim((string)$this->request->post('city', '贵阳市'));
$district = trim((string)$this->request->post('district', ''));
$detailAddr = trim((string)$this->request->post('address'));
$floor = max(1, (int)$this->request->post('floor', 1));
$hasElevator = (int)$this->request->post('has_elevator', 1);
if ($contact && $mobile && $detailAddr) {
$addressId = Db::name('address')->insertGetId([
'user_id' => $this->uid(),
'contact' => $contact,
'mobile' => $mobile,
'province' => $province,
'city' => $city,
'district' => $district,
'address' => $detailAddr,
'floor' => $floor,
'has_elevator' => $hasElevator,
'createtime' => time(),
'updatetime' => time(),
]);
$address = Db::name('address')->where('id', $addressId)->find();
}
}
if (!$address) {
$this->error('请选择或填写完整服务地址');
}
if ($appointTime > 0 && $appointTime < time()) {
$this->error('预约时间不能早于当前时间');
}
// 多商品组合名称、总件数与图片汇总
$serviceName = $service ? $service['name'] : '同城综合上门服务';
$catId = $service ? (int)$service['cat_id'] : 1;
$serviceImage = $service ? $service['image'] : '';
if (!empty($goodsList) && is_array($goodsList)) {
$firstG = reset($goodsList);
if (!empty($firstG['cat_id'])) {
$catId = (int)$firstG['cat_id'];
}
$names = [];
$totalCount = 0;
$allImages = [];
foreach ($goodsList as $idx => $g) {
$gName = !empty($g['custom_name']) ? $g['custom_name'] : (isset($g['item_name']) ? $g['item_name'] : (isset($g['goods_name']) ? $g['goods_name'] : (isset($g['name']) ? $g['name'] : '服务项')));
$gNum = isset($g['num']) ? (int)$g['num'] : 1;
$names[] = $gName . 'x' . $gNum;
$totalCount += $gNum;
if (!empty($g['images']) && is_array($g['images'])) {
$allImages = array_merge($allImages, $g['images']);
}
}
if (!empty($names)) {
$serviceName = implode(' + ', array_slice($names, 0, 3)) . (count($names) > 3 ? ' 等' : '');
}
if ($totalCount > 0) {
$goodsNum = $totalCount;
}
if (!empty($allImages)) {
$goodsImages = $allImages;
}
}
// 金额统一由 Home::calcAmount 计算,保证与试算口径一致
$amount = Home::calcAmount($service, [
'floor' => (int)$address['floor'],
'has_elevator' => (int)$address['has_elevator'],
'urgent' => $urgent,
], $couponId, $this->uid(), $priceType, $userPrice, $goodsList);
if (empty($serviceImage) && !empty($goodsList)) {
foreach ($goodsList as $item) {
if (!empty($item['goods_image'])) {
$serviceImage = $item['goods_image'];
break;
} elseif (!empty($item['images']) && is_array($item['images'])) {
$serviceImage = reset($item['images']);
break;
} elseif (!empty($item['service_id'])) {
$subSvc = Db::name('service')->where('id', (int)$item['service_id'])->find();
if ($subSvc && !empty($subSvc['image'])) {
$serviceImage = $subSvc['image'];
break;
}
}
}
}
if (empty($serviceImage) && $service && !empty($service['image'])) {
$serviceImage = $service['image'];
}
$amount['price_type'] = $priceType;
$orderNo = $this->makeOrderNo();
$verifyCode = (string)mt_rand(1000, 9999);
// 如果有多商品明细,结构化存入 goods_images 或在 remark 补充
$goodsImagesPayload = is_array($goodsImages) ? json_encode($goodsImages, JSON_UNESCAPED_UNICODE) : (string)$goodsImages;
if (!empty($goodsList)) {
$extraPayload = [
'goods_list' => $goodsList,
'images' => is_array($goodsImages) ? $goodsImages : [],
];
$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(),
'master_id' => 0,
'cat_id' => $catId,
'service_id' => $service ? (int)$service['id'] : 0,
'service_name' => $serviceName,
'service_image' => $serviceImage,
'price_type' => $priceType,
'goods_images' => $goodsImagesPayload,
'goods_model' => mb_substr($goodsModel, 0, 100),
'goods_num' => $goodsNum,
'floor' => (int)$address['floor'],
'has_elevator' => (int)$address['has_elevator'],
'urgent' => $urgent ? 1 : 0,
'remark' => mb_substr($remark, 0, 500),
'contact' => $address['contact'],
'mobile' => $address['mobile'],
'province' => $address['province'],
'city' => $address['city'],
'district' => $address['district'],
'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'],
'urgent_amount' => $amount['urgent_amount'],
'discount_amount' => $amount['discount_amount'],
'total_amount' => $amount['total_amount'],
'pay_amount' => $amount['pay_amount'],
'status' => $priceType === 'quote' ? OrderStatus::QUOTING : OrderStatus::PENDING_PAY,
'pay_status' => 0,
'coupon_id' => $amount['coupon'] ? (int)$amount['coupon']['id'] : 0,
'verify_code' => $verifyCode,
'createtime' => time(),
'updatetime' => time(),
]);
$modeText = [
'fixed' => '一口价',
'quote' => '师傅报价',
'custom' => '用户自主定价',
'range' => '按面积计价',
];
if (!empty($goodsList) && is_array($goodsList)) {
foreach ($goodsList as $g) {
// 商品名称快照:优先读取自填名称 custom_name 或 item_name,确保非标自填不丢失
$finalGoodsName = '';
if (!empty($g['custom_name'])) {
$finalGoodsName = trim((string)$g['custom_name']);
} elseif (!empty($g['item_name'])) {
$finalGoodsName = trim((string)$g['item_name']);
} elseif (!empty($g['goods_name'])) {
$finalGoodsName = trim((string)$g['goods_name']);
} else {
$finalGoodsName = (string)$serviceName;
}
// 规格型号快照:优先读取 spec_name 或 goods_model
$finalSpecModel = '';
if (!empty($g['spec_name'])) {
$finalSpecModel = trim((string)$g['spec_name']);
} elseif (!empty($g['goods_model'])) {
$finalSpecModel = trim((string)$g['goods_model']);
} else {
$finalSpecModel = (string)$goodsModel;
}
// 商品实物图/现场图
$finalImage = '';
if (!empty($g['goods_image'])) {
$finalImage = (string)$g['goods_image'];
} elseif (!empty($g['images']) && is_array($g['images'])) {
$finalImage = (string)reset($g['images']);
}
// 增项服务快照
$addonStr = '';
if (!empty($g['addon_services'])) {
$addonStr = is_array($g['addon_services']) ? json_encode($g['addon_services'], JSON_UNESCAPED_UNICODE) : (string)$g['addon_services'];
} elseif (!empty($g['extras']) && is_array($g['extras'])) {
$hitExtras = array_filter($g['extras'], function($ex) {
return !empty($ex['num']) && (int)$ex['num'] > 0;
});
if (!empty($hitExtras)) {
$addonStr = json_encode(array_values($hitExtras), JSON_UNESCAPED_UNICODE);
}
}
Db::name('order_goods')->insert([
'order_id' => (int)$id,
'service_id' => isset($g['service_id']) ? (int)$g['service_id'] : (int)$serviceId,
'goods_name' => mb_substr($finalGoodsName, 0, 100),
'goods_model' => mb_substr($finalSpecModel, 0, 100),
'goods_num' => isset($g['num']) ? max(1, (int)$g['num']) : (isset($g['goods_num']) ? max(1, (int)$g['goods_num']) : 1),
'goods_image' => mb_substr($finalImage, 0, 255),
'addon_services' => mb_substr($addonStr, 0, 255),
'base_price' => isset($g['base_price']) ? (float)$g['base_price'] : (float)$amount['base_amount'],
'createtime' => time(),
]);
}
}
$initStatus = $priceType === 'quote' ? OrderStatus::QUOTING : OrderStatus::PENDING_PAY;
$this->addLog($id, $initStatus, '用户下单(' . (isset($modeText[$priceType]) ? $modeText[$priceType] : '一口价') . ')', 'user', $this->uid());
$this->success('下单成功', [
'order_id' => (int)$id,
'order_no' => $orderNo,
'amount' => $amount,
]);
}
/**
* 支付(联调模拟支付,pay_type=mock;上线换微信支付回调)
* @ApiMethod (POST)
*/
public function pay()
{
$orderId = (int)$this->request->post('order_id');
$payType = $this->request->post('pay_type', 'mock');
$order = $this->ownOrder($orderId, 'user');
if ((int)$order['pay_status'] === 1) {
$this->error('订单已支付,请勿重复支付');
}
if ((int)$order['status'] !== OrderStatus::PENDING_PAY) {
$this->error('订单状态不允许支付');
}
Db::startTrans();
try {
Db::name('order')->where('id', $orderId)->update([
'pay_status' => 1,
'pay_type' => $payType,
'pay_time' => time(),
'transaction_id' => 'MOCK' . date('YmdHis') . mt_rand(1000, 9999),
'updatetime' => time(),
]);
$nextStatus = $order['price_type'] === 'quote' ? OrderStatus::QUOTING : OrderStatus::PENDING_ACCEPT;
$nextRemark = $nextStatus === OrderStatus::QUOTING
? '支付成功,订单已进入报价大厅,等待师傅报价'
: '支付成功,订单已进入接单大厅';
$this->transit($order, $nextStatus, $nextRemark, [], 'user', $this->uid());
$this->addLog($orderId, $nextStatus, $nextStatus === OrderStatus::QUOTING ? '订单进入师傅报价大厅' : '订单进入师傅接单大厅', 'system', 0);
// 核销优惠券
if ((int)$order['coupon_id'] > 0) {
Db::name('user_coupon')->where('id', (int)$order['coupon_id'])->update([
'status' => 'used',
'order_id' => $orderId,
'use_time' => time(),
]);
}
Db::commit();
} catch (\Exception $e) {
Db::rollback();
$this->error('支付失败:' . $e->getMessage());
}
// 订单广播通知:进入接单大厅后,通知周边在线师傅
if ($nextStatus === OrderStatus::PENDING_ACCEPT) {
try {
$masters = Db::name('master')->where('status', 'normal')->limit(20)->select();
foreach ($masters as $m) {
$this->notify('master', (int)$m['user_id'], '新工单就绪通知', '同城有新工单【' . $order['order_no'] . '】进入接单大厅,请尽快前往抢单!', 'order', $orderId);
}
} catch (\Exception $e) {}
}
$this->success('支付成功');
}
/**
* 我的订单列表
*/
/**
* 用户在线支付施工增项补差价
* @ApiMethod (POST)
*/
public function payDiff()
{
$orderId = (int)$this->request->post('order_id');
$order = $this->ownOrder($orderId, 'user');
if ((int)$order['diff_status'] !== 1 || (float)$order['diff_amount'] <= 0) {
$this->error('当前订单无待支付的增项补差价');
}
$diffAmount = (float)$order['diff_amount'];
$newPayAmount = round((float)$order['pay_amount'] + $diffAmount, 2);
Db::startTrans();
try {
Db::name('order')->where('id', $orderId)->update([
'diff_status' => 2,
'pay_amount' => $newPayAmount,
'updatetime' => time(),
]);
$this->addLog($orderId, (int)$order['status'], '用户在线支付施工增项补差价 ¥' . $diffAmount, 'user', $this->uid());
Db::commit();
} catch (\Exception $e) {
Db::rollback();
$this->error('支付失败:' . $e->getMessage());
}
if ((int)$order['master_id'] > 0) {
$m = Db::name('master')->where('id', $order['master_id'])->find();
if ($m) {
$this->notify('master', (int)$m['user_id'], '客户已支付增项补差价', '工单 ' . $order['order_no'] . ' 客户已支付增项补差价 ¥' . $diffAmount, 'order', $orderId);
}
}
$this->success('增项补差价支付成功!', ['pay_amount' => $newPayAmount]);
}
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)));
$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' || $status === '8') {
$q->where('status', OrderStatus::COMPLETED);
} 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);
}
return $q;
};
$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('订单已删除');
}
/**
* 一键晒单 / 取消晒单(仅限已完成订单,控制是否在首页服务动态公开展示)
* @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,
]);
}
/**
* 订单详情(含状态轨迹与师傅信息)
*/
public function detail()
{
$orderId = (int)$this->request->param('id') ?: (int)$this->request->param('order_id');
if ($orderId <= 0) {
$this->error('订单参数错误');
}
$order = Db::name('order')->where('id', $orderId)->find();
if (!$order) {
$this->error('订单不存在');
}
// 本人订单,或同一商家(主账号 + 关联账号)的订单
if ((int)$order['user_id'] !== $this->uid()) {
$uids = $this->currentMerchantUserIds();
if (!in_array((int)$order['user_id'], $uids, true)) {
$this->error('无权操作该订单', null, 403);
}
}
$logs = Db::name('order_log')->where('order_id', $orderId)->order('id asc')->select();
foreach ($logs as &$log) {
$log['status_text'] = OrderStatus::label($log['status']);
$log['time_text'] = date('m-d H:i', (int)$log['createtime']);
}
unset($log);
$master = null;
if ((int)$order['master_id'] > 0) {
$m = Db::name('master')->where('id', $order['master_id'])->find();
if ($m) {
$master = [
'id' => (int)$m['id'],
'realname' => $m['realname'],
'mobile' => substr_replace($m['mobile'], '****', 3, 4),
'score' => (float)$m['score'],
'order_count' => (int)$m['order_count'],
'level' => (int)$m['level'],
];
}
}
$media = Db::name('order_media')->where('order_id', $orderId)->order('id asc')->select();
// 报价订单:返回师傅报价列表,供用户比价并选择师傅
$quotes = [];
if ($order['price_type'] === 'quote') {
$rows = Db::name('order_quote')->alias('q')
->join('master m', 'm.id = q.master_id', 'LEFT')
->field('q.id,q.order_id,q.master_id,q.amount,q.description,q.valid_hours,q.expire_time,q.status,q.createtime,m.realname,m.mobile,m.avatar,m.score,m.order_count,m.level')
->where('q.order_id', $orderId)
->order('q.amount asc,q.id asc')
->select();
$statusText = ['wait' => '等待选择', 'accept' => '已选中', 'reject' => '未选中', 'expired' => '已过期'];
$now = time();
foreach ($rows as $q) {
if ($q['status'] === 'wait' && (int)$q['expire_time'] > 0 && (int)$q['expire_time'] < $now) {
Db::name('order_quote')->where('id', (int)$q['id'])->update(['status' => 'expired']);
$q['status'] = 'expired';
}
$quotes[] = [
'id' => (int)$q['id'],
'master_id' => (int)$q['master_id'],
'realname' => $q['realname'] ?: '师傅',
'avatar' => $q['avatar'],
'score' => (float)$q['score'],
'order_count' => (int)$q['order_count'],
'level' => (int)$q['level'],
'amount' => (float)$q['amount'],
'description' => $q['description'],
'valid_hours' => (int)$q['valid_hours'],
'status' => $q['status'],
'status_text' => isset($statusText[$q['status']]) ? $statusText[$q['status']] : $q['status'],
'time_text' => date('m-d H:i', (int)$q['createtime']),
'expire_text' => (int)$q['expire_time'] > 0 ? date('m-d H:i', (int)$q['expire_time']) : '长期有效',
];
}
}
$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 = [
[
// 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'],
'addon_services' => '安装x' . max(1, (int)$formattedOrder['goods_num']) . ';',
'base_price' => (float)$formattedOrder['pay_amount'],
]
];
}
$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,
'master' => $master,
'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,
]);
}
/**
* 报价订单:用户选择师傅报价(选定后订单进入已接单)
* @ApiMethod (POST)
*/
public function chooseQuote()
{
$orderId = (int)$this->request->post('order_id');
$quoteId = (int)$this->request->post('quote_id');
$order = $this->ownOrder($orderId, 'user');
if ($order['price_type'] !== 'quote') {
$this->error('该订单不是师傅报价订单');
}
if ((int)$order['status'] !== OrderStatus::QUOTING) {
$this->error('当前状态(' . OrderStatus::label((int)$order['status']) . ')不可选择报价');
}
if ((int)$order['master_id'] > 0) {
$this->error('已选定师傅,请勿重复选择');
}
$quote = Db::name('order_quote')->where('id', $quoteId)->where('order_id', $orderId)->find();
if (!$quote) {
$this->error('报价不存在');
}
if ($quote['status'] !== 'wait') {
$this->error('该报价已失效,请选择其他师傅');
}
if ((int)$quote['expire_time'] > 0 && (int)$quote['expire_time'] < time()) {
Db::name('order_quote')->where('id', $quoteId)->update(['status' => 'expired']);
$this->error('该报价已过期,请选择其他师傅');
}
$master = Db::name('master')->where('id', (int)$quote['master_id'])->find();
if (!$master) {
$this->error('师傅信息不存在');
}
$quoteAmount = round((float)$quote['amount'], 2);
$payAmount = max(0, round($quoteAmount - (float)$order['discount_amount'], 2));
$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 {
$updateData = [
'master_id' => (int)$quote['master_id'],
'accept_time' => time(),
'base_amount' => $quoteAmount,
'floor_amount' => 0,
'urgent_amount' => 0,
'total_amount' => $quoteAmount,
'pay_amount' => $payAmount,
'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)
->where('status', 'wait')->update(['status' => 'reject']);
$this->notify('user', (int)$order['user_id'], '已选定服务师傅', $remark, 'order', $orderId);
$this->notify('master', (int)$master['user_id'], '您的报价已被选中', '订单 ' . $order['order_no'] . ' 客户已选择您的报价 ¥' . $quoteAmount . ',请尽快联系客户', 'order', $orderId);
Db::commit();
} catch (\Exception $e) {
Db::rollback();
$this->error('选择报价失败:' . $e->getMessage());
}
$this->success('已选定师傅并完成支付托管,请等待师傅联系', [
'pay_amount' => $payAmount,
'need_diff' => $needDiff,
'already_pay' => $alreadyPaid,
]);
}
/**
* 报价订单:报价列表(详情页轮询/刷新使用)
*/
public function quotes()
{
$orderId = (int)$this->request->param('order_id');
$order = $this->ownOrder($orderId, 'user');
if ($order['price_type'] !== 'quote') {
$this->success('', ['list' => [], 'can_choose' => false]);
}
$rows = Db::name('order_quote')->alias('q')
->join('master m', 'm.id = q.master_id', 'LEFT')
->field('q.id,q.master_id,q.amount,q.description,q.valid_hours,q.expire_time,q.status,q.createtime,m.realname,m.avatar,m.score,m.order_count,m.level')
->where('q.order_id', $orderId)
->order('q.amount asc,q.id asc')
->select();
$statusText = ['wait' => '等待选择', 'accept' => '已选中', 'reject' => '未选中', 'expired' => '已过期'];
$list = [];
foreach ($rows as $q) {
$list[] = [
'id' => (int)$q['id'],
'master_id' => (int)$q['master_id'],
'realname' => $q['realname'] ?: '师傅',
'avatar' => $q['avatar'],
'score' => (float)$q['score'],
'order_count' => (int)$q['order_count'],
'amount' => (float)$q['amount'],
'description' => $q['description'],
'status' => $q['status'],
'status_text' => isset($statusText[$q['status']]) ? $statusText[$q['status']] : $q['status'],
'time_text' => date('m-d H:i', (int)$q['createtime']),
'expire_text' => (int)$q['expire_time'] > 0 ? date('m-d H:i', (int)$q['expire_time']) : '长期有效',
];
}
$this->success('', [
'list' => $list,
'can_choose' => (int)$order['status'] === OrderStatus::QUOTING && (int)$order['master_id'] === 0,
]);
}
/**
* 取消订单(已支付则模拟退款)
* @ApiMethod (POST)
*/
public function cancel()
{
$orderId = (int)$this->request->post('order_id');
$reason = trim((string)$this->request->post('reason', '用户取消'));
$order = $this->ownOrder($orderId, 'user');
if (!OrderStatus::canCancelByUser($order['status'])) {
$this->error('当前状态(' . OrderStatus::label($order['status']) . ')不可取消,请联系客服');
}
Db::startTrans();
try {
$extra = ['cancel_reason' => mb_substr($reason, 0, 255)];
if ((int)$order['pay_status'] === 1) {
$extra['pay_status'] = 2;
$extra['refund_amount'] = $order['pay_amount'];
}
$this->transit($order, OrderStatus::CANCELLED, '用户取消:' . $reason, $extra, 'user', $this->uid());
if ((int)$order['coupon_id'] > 0) {
Db::name('user_coupon')->where('id', (int)$order['coupon_id'])->update([
'status' => 'unused', 'order_id' => 0, 'use_time' => null,
]);
}
Db::commit();
} catch (\Exception $e) {
Db::rollback();
$this->error('取消失败:' . $e->getMessage());
}
$this->success('订单已取消');
}
/**
* 客户验收(验收码可选校验)→ 生成结算并给师傅入账
* @ApiMethod (POST)
*/
public function verify()
{
$orderId = (int)$this->request->post('order_id');
$code = trim((string)$this->request->post('verify_code', ''));
$order = $this->ownOrder($orderId, 'user');
if ((int)$order['status'] !== OrderStatus::PENDING_VERIFY) {
$this->error('当前状态(' . OrderStatus::label($order['status']) . ')不可验收');
}
if ($code !== '' && $code !== (string)$order['verify_code']) {
$this->error('验收码不正确');
}
$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 {
$this->transit($order, OrderStatus::COMPLETED, '客户验收通过,订单完成', [
'verify_time' => time(),
'settle_status' => 1,
'settle_amount' => $settleAmount,
], 'user', $this->uid());
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();
$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(),
]);
$this->notify('master', (int)$master['user_id'], '订单结算入账', '订单 ' . $order['order_no'] . ' 已验收,收入 ¥' . $settleAmount, 'order', $orderId);
}
Db::commit();
} catch (\Exception $e) {
Db::rollback();
$this->error('验收失败:' . $e->getMessage());
}
$this->success('验收成功,订单已完成');
}
/**
* 订单输出格式化
*/
protected function formatOrder($row)
{
$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),
'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_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'],
'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,
]);
}
/**
* 当前用户所属商家的全部账号(主账号 + 关联账号)
*/
protected function currentMerchantUserIds()
{
$merchant = Db::name('merchant')->where('user_id', $this->uid())->find();
if (!$merchant) {
$bind = Db::name('merchant_member')->where('user_id', $this->uid())->find();
if ($bind) {
$merchant = Db::name('merchant')->where('id', (int)$bind['merchant_id'])->find();
}
}
if (!$merchant) {
return [];
}
$ids = [(int)$merchant['user_id']];
$rows = Db::name('merchant_member')->where('merchant_id', (int)$merchant['id'])->column('user_id');
foreach ($rows as $id) {
$id = (int)$id;
if ($id > 0 && !in_array($id, $ids, true)) {
$ids[] = $id;
}
}
return $ids;
}
/**
* 计价方式文案(前后端与后台统一口径)
*/
protected function priceTypeText($type)
{
$map = [
'fixed' => '一口价',
'quote' => '师傅报价',
'custom' => '用户自主定价',
'range' => '按面积计价',
];
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;
}
}