feat(order): 下单支持一口价/师傅报价竞价/用户自主定价三种方式

- 下单页新增计价方式选择;自主定价填写总价,师傅报价单进入报价大厅
- 用户订单详情展示师傅报价列表并可选定师傅;选定后同步师傅、金额与订单状态
- 师傅大厅新增用户定价筛选与按价接单;报价单禁止直接抢单
- 后端订单新增 custom 价态、金额口径统一由 calcAmount 输出
- 报价中状态纳入用户可取消范围;超时回流重新开放报价
- 新增 fa_order.price_type custom 枚举迁移脚本(schema_phase6)
这个提交包含在:
linda
2026-10-28 17:48:23 +08:00
父节点 3bba2d6e94
当前提交 e1bf139c5a
共有 11 个文件被更改,包括 500 次插入 和 50 次删除

查看文件

@@ -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] : '一口价';
}
}