feat(order): 下单支持一口价/师傅报价竞价/用户自主定价三种方式
- 下单页新增计价方式选择;自主定价填写总价,师傅报价单进入报价大厅 - 用户订单详情展示师傅报价列表并可选定师傅;选定后同步师傅、金额与订单状态 - 师傅大厅新增用户定价筛选与按价接单;报价单禁止直接抢单 - 后端订单新增 custom 价态、金额口径统一由 calcAmount 输出 - 报价中状态纳入用户可取消范围;超时回流重新开放报价 - 新增 fa_order.price_type custom 枚举迁移脚本(schema_phase6)
这个提交包含在:
@@ -39,6 +39,20 @@ class Order extends Base
|
||||
$this->error('服务不存在或已下架');
|
||||
}
|
||||
|
||||
// 下单页可选择:一口价 / 师傅报价 / 用户自主定价(未传则沿用服务默认)
|
||||
$priceType = trim((string)$this->request->post('price_type', $service['price_type']));
|
||||
if (!in_array($priceType, ['fixed', 'quote', 'custom', 'range'], true)) {
|
||||
$priceType = $service['price_type'];
|
||||
}
|
||||
$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 = Db::name('address')->where('id', $addressId)->where('user_id', $this->uid())->find();
|
||||
if (!$address) {
|
||||
$this->error('请选择服务地址');
|
||||
@@ -52,7 +66,8 @@ class Order extends Base
|
||||
'floor' => (int)$address['floor'],
|
||||
'has_elevator' => (int)$address['has_elevator'],
|
||||
'urgent' => $urgent,
|
||||
], $couponId, $this->uid());
|
||||
], $couponId, $this->uid(), $priceType, $userPrice);
|
||||
$amount['price_type'] = $priceType;
|
||||
|
||||
$orderNo = $this->makeOrderNo();
|
||||
$verifyCode = (string)mt_rand(1000, 9999);
|
||||
@@ -65,7 +80,7 @@ class Order extends Base
|
||||
'service_id' => (int)$service['id'],
|
||||
'service_name' => $service['name'],
|
||||
'service_image' => $service['image'],
|
||||
'price_type' => $service['price_type'],
|
||||
'price_type' => $priceType,
|
||||
'goods_images' => is_array($goodsImages) ? json_encode($goodsImages, JSON_UNESCAPED_UNICODE) : (string)$goodsImages,
|
||||
'goods_model' => mb_substr($goodsModel, 0, 100),
|
||||
'goods_num' => $goodsNum,
|
||||
@@ -96,7 +111,13 @@ class Order extends Base
|
||||
'updatetime' => time(),
|
||||
]);
|
||||
|
||||
$this->addLog($id, OrderStatus::PENDING_PAY, '用户下单', 'user', $this->uid());
|
||||
$modeText = [
|
||||
'fixed' => '一口价',
|
||||
'quote' => '师傅报价',
|
||||
'custom' => '用户自主定价',
|
||||
'range' => '按面积计价',
|
||||
];
|
||||
$this->addLog($id, OrderStatus::PENDING_PAY, '用户下单(' . (isset($modeText[$priceType]) ? $modeText[$priceType] : '一口价') . ')', 'user', $this->uid());
|
||||
|
||||
$this->success('下单成功', [
|
||||
'order_id' => (int)$id,
|
||||
@@ -131,8 +152,12 @@ class Order extends Base
|
||||
'transaction_id' => 'MOCK' . date('YmdHis') . mt_rand(1000, 9999),
|
||||
'updatetime' => time(),
|
||||
]);
|
||||
$this->transit($order, OrderStatus::PENDING_ACCEPT, '支付成功,订单已进入接单大厅', [], 'user', $this->uid());
|
||||
$this->addLog($orderId, OrderStatus::PENDING_ACCEPT, '订单进入师傅接单大厅', 'system', 0);
|
||||
$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) {
|
||||
@@ -217,11 +242,161 @@ class Order extends Base
|
||||
|
||||
$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']) : '长期有效',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$this->success('', [
|
||||
'order' => $this->formatOrder($order),
|
||||
'master' => $master,
|
||||
'logs' => $logs,
|
||||
'media' => $media,
|
||||
'quotes' => $quotes,
|
||||
'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));
|
||||
$remark = '客户已选择 ' . $master['realname'] . ' 的报价 ¥' . $quoteAmount . ',等待师傅联系';
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
$this->transit($order, OrderStatus::ACCEPTED, $remark, [
|
||||
'master_id' => (int)$quote['master_id'],
|
||||
'accept_time' => time(),
|
||||
'base_amount' => $quoteAmount,
|
||||
'floor_amount' => 0,
|
||||
'urgent_amount' => 0,
|
||||
'total_amount' => $quoteAmount,
|
||||
'pay_amount' => $payAmount,
|
||||
], '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('已选定师傅,请等待师傅联系');
|
||||
}
|
||||
|
||||
/**
|
||||
* 报价订单:报价列表(详情页轮询/刷新使用)
|
||||
*/
|
||||
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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -355,7 +530,22 @@ class Order extends Base
|
||||
'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']),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计价方式文案(前后端与后台统一口径)
|
||||
*/
|
||||
protected function priceTypeText($type)
|
||||
{
|
||||
$map = [
|
||||
'fixed' => '一口价',
|
||||
'quote' => '师傅报价',
|
||||
'custom' => '用户自主定价',
|
||||
'range' => '按面积计价',
|
||||
];
|
||||
return isset($map[$type]) ? $map[$type] : '一口价';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户