feat(order): 下单支持一口价/师傅报价竞价/用户自主定价三种方式
- 下单页新增计价方式选择;自主定价填写总价,师傅报价单进入报价大厅 - 用户订单详情展示师傅报价列表并可选定师傅;选定后同步师傅、金额与订单状态 - 师傅大厅新增用户定价筛选与按价接单;报价单禁止直接抢单 - 后端订单新增 custom 价态、金额口径统一由 calcAmount 输出 - 报价中状态纳入用户可取消范围;超时回流重新开放报价 - 新增 fa_order.price_type custom 枚举迁移脚本(schema_phase6)
这个提交包含在:
@@ -127,7 +127,7 @@ CREATE TABLE IF NOT EXISTS `fa_order` (
|
||||
`service_id` int(10) unsigned NOT NULL DEFAULT '0',
|
||||
`service_name` varchar(100) NOT NULL DEFAULT '' COMMENT '服务名称(快照)',
|
||||
`service_image` varchar(255) NOT NULL DEFAULT '' COMMENT '服务主图(快照)',
|
||||
`price_type` enum('fixed','quote','range') NOT NULL DEFAULT 'fixed',
|
||||
`price_type` enum('fixed','quote','range','custom') NOT NULL DEFAULT 'fixed' COMMENT '一口价/报价竞价/区间/用户自主定价',
|
||||
`goods_images` varchar(1000) NOT NULL DEFAULT '' COMMENT '商品图片(json)',
|
||||
`goods_model` varchar(100) NOT NULL DEFAULT '' COMMENT '商品型号',
|
||||
`goods_num` int(11) NOT NULL DEFAULT '1',
|
||||
|
||||
15
db/schema_phase6.sql
普通文件
15
db/schema_phase6.sql
普通文件
@@ -0,0 +1,15 @@
|
||||
-- ============================================================================
|
||||
-- 优匠优服 · Phase 6 下单三种计价方式
|
||||
-- 一口价 fixed / 师傅报价竞价 quote / 用户自主定价 custom
|
||||
-- fa_service 仍使用 fixed / quote / range(服务默认价态)
|
||||
-- 下单时用户可覆盖 price_type,因此 fa_order 增加 custom
|
||||
-- ============================================================================
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
ALTER TABLE `fa_order`
|
||||
MODIFY COLUMN `price_type` enum('fixed','quote','range','custom') NOT NULL DEFAULT 'fixed'
|
||||
COMMENT '一口价/师傅报价竞价/区间/用户自主定价';
|
||||
|
||||
-- 回滚(先确认没有 custom 订单):
|
||||
-- SELECT COUNT(*) FROM fa_order WHERE price_type='custom';
|
||||
-- ALTER TABLE fa_order MODIFY COLUMN price_type enum('fixed','quote','range') NOT NULL DEFAULT 'fixed';
|
||||
@@ -23,6 +23,8 @@
|
||||
<view class="item" v-for="o in list" :key="o.id">
|
||||
<view class="hd">
|
||||
<text class="tag-urgent" v-if="Number(o.urgent)">急单</text>
|
||||
<text class="tag-mode quote" v-if="o.price_type === 'quote'">师傅报价</text>
|
||||
<text class="tag-mode custom" v-if="o.price_type === 'custom'">用户定价</text>
|
||||
<text class="svc">{{ o.service_name }}</text>
|
||||
<text class="amt">¥{{ o.pay_amount }}</text>
|
||||
</view>
|
||||
@@ -36,7 +38,7 @@
|
||||
<text class="phone">{{ o.left_seconds > 0 ? ('剩余 ' + countdown(o.left_seconds)) : '即将关闭' }}</text>
|
||||
<view class="ft-btns">
|
||||
<view class="quote-btn" v-if="o.price_type === 'quote'" @tap="goQuote(o)">我要报价({{ o.quote_count }})</view>
|
||||
<view class="grab" v-else @tap="grab(o)">立即抢单</view>
|
||||
<view class="grab" v-else @tap="grab(o)">{{ o.price_type === 'custom' ? '按此价接单' : '立即抢单' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -64,7 +66,8 @@
|
||||
const typeTabs = [
|
||||
{ key: 'all', name: '全部订单' },
|
||||
{ key: 'fixed', name: '一口价' },
|
||||
{ key: 'quote', name: '报价订单' }
|
||||
{ key: 'quote', name: '报价订单' },
|
||||
{ key: 'custom', name: '用户定价' }
|
||||
];
|
||||
const sortTabs = [
|
||||
{ key: 'new', name: '最新' },
|
||||
@@ -147,6 +150,9 @@
|
||||
.item { background: #fff; border-radius: 20rpx; margin-bottom: 20rpx; padding: 22rpx 24rpx; box-shadow: 0 6rpx 20rpx rgba(240,129,31,0.06); }
|
||||
.hd { display: flex; align-items: center; padding-bottom: 14rpx; border-bottom: 1rpx solid #F4F4F4; }
|
||||
.tag-urgent { padding: 0 12rpx; line-height: 34rpx; border-radius: 8rpx; background: #FDEEEB; color: #E8331B; font-size: 20rpx; margin-right: 10rpx; }
|
||||
.tag-mode { padding: 0 12rpx; line-height: 34rpx; border-radius: 8rpx; font-size: 20rpx; margin-right: 10rpx; }
|
||||
.tag-mode.quote { background: #F0EDFF; color: #5B3FD8; }
|
||||
.tag-mode.custom { background: #E9F7EE; color: #2FA84F; }
|
||||
.svc { flex: 1; font-size: 30rpx; font-weight: 700; color: #1F1F1F; }
|
||||
.amt { font-size: 32rpx; font-weight: 700; color: #F0710D; }
|
||||
.bd { display: flex; flex-direction: column; padding: 16rpx 0; }
|
||||
|
||||
@@ -6,11 +6,38 @@
|
||||
<text class="svc-name">{{ svc ? svc.name : '' }}</text>
|
||||
<text class="svc-price">{{ svc ? svc.price_text : '' }}</text>
|
||||
</view>
|
||||
<view class="urgent" :class="{ on: urgent }" @tap="urgent = !urgent">
|
||||
<view class="urgent" :class="{ on: urgent }" v-if="priceType !== 'custom'" @tap="urgent = !urgent">
|
||||
<text class="urgent-txt">{{ urgent ? '已加急 +20%' : '加急' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 计价方式:一口价 / 师傅报价竞价 / 用户自主定价 -->
|
||||
<view class="sec">
|
||||
<view class="sec-title">选择下单方式</view>
|
||||
<view class="modes">
|
||||
<view
|
||||
v-for="m in modes"
|
||||
:key="m.key"
|
||||
class="mode"
|
||||
:class="{ on: priceType === m.key }"
|
||||
@tap="selectMode(m.key)"
|
||||
>
|
||||
<text class="mode-name">{{ m.name }}</text>
|
||||
<text class="mode-desc">{{ m.desc }}</text>
|
||||
<view class="mode-check" v-if="priceType === m.key">✓</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mode-note">{{ modeNote }}</view>
|
||||
|
||||
<view class="fi" v-if="priceType === 'custom'">
|
||||
<text class="fk">我的出价</text>
|
||||
<view class="price-input-wrap">
|
||||
<text class="price-unit">¥</text>
|
||||
<input class="fv price-input" v-model="userPrice" type="digit" placeholder="请输入您愿意支付的总价" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="sec">
|
||||
<view class="sec-title">服务地址</view>
|
||||
<view class="addr" v-if="address">
|
||||
@@ -61,22 +88,42 @@
|
||||
</view>
|
||||
|
||||
<view class="sec" v-if="amount">
|
||||
<view class="sec-title">费用明细</view>
|
||||
<view class="li"><text class="k">基础价</text><text class="v">¥{{ amount.base_amount }}</text></view>
|
||||
<view class="li"><text class="k">楼层费</text><text class="v">¥{{ amount.floor_amount }}</text></view>
|
||||
<view class="li" v-if="amount.urgent_amount > 0"><text class="k">加急费</text><text class="v">¥{{ amount.urgent_amount }}</text></view>
|
||||
<view class="li total"><text class="k">应付合计</text><text class="v big">¥{{ amount.pay_amount }}</text></view>
|
||||
<view class="sec-title">{{ priceType === 'quote' ? '费用预估' : '费用明细' }}</view>
|
||||
<view class="li">
|
||||
<text class="k">{{ priceType === 'custom' ? '您的出价' : '基础价' }}</text>
|
||||
<text class="v">¥{{ amount.base_amount }}</text>
|
||||
</view>
|
||||
<view class="li" v-if="priceType !== 'custom'">
|
||||
<text class="k">楼层费</text>
|
||||
<text class="v">¥{{ amount.floor_amount }}</text>
|
||||
</view>
|
||||
<view class="li" v-if="priceType !== 'custom' && amount.urgent_amount > 0">
|
||||
<text class="k">加急费</text>
|
||||
<text class="v">¥{{ amount.urgent_amount }}</text>
|
||||
</view>
|
||||
<view class="li" v-if="amount.discount_amount > 0">
|
||||
<text class="k">优惠券抵扣</text>
|
||||
<text class="v">-¥{{ amount.discount_amount }}</text>
|
||||
</view>
|
||||
<view class="li total">
|
||||
<text class="k">{{ priceType === 'quote' ? '参考合计' : '应付合计' }}</text>
|
||||
<text class="v big">¥{{ amount.pay_amount }}</text>
|
||||
</view>
|
||||
<text class="fee-note" v-if="priceType === 'quote'">先支付参考价,选定师傅报价后按最终报价结算</text>
|
||||
<text class="fee-note" v-if="priceType === 'custom'">自主定价为含楼层/加急的总价,师傅接单即按此价服务</text>
|
||||
</view>
|
||||
|
||||
<view class="bar">
|
||||
<view class="bar-l"><text class="bar-k">合计</text><text class="bar-p">¥{{ amount ? amount.pay_amount : 0 }}</text></view>
|
||||
<view class="bar-btn" :class="{ disabled: submitting }" @tap="submit">{{ submitting ? '提交中...' : '提交并支付' }}</view>
|
||||
<view class="bar-l"><text class="bar-k">{{ priceType === 'quote' ? '参考价' : '合计' }}</text><text class="bar-p">¥{{ amount ? amount.pay_amount : 0 }}</text></view>
|
||||
<view class="bar-btn" :class="{ disabled: submitting }" @tap="submit">
|
||||
{{ submitting ? '提交中...' : (priceType === 'quote' ? '发布需求' : '提交并支付') }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { ref, reactive, watch, computed } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import api from '@/utils/api.js';
|
||||
import { isLogin, toLogin } from '@/utils/request.js';
|
||||
@@ -88,6 +135,21 @@
|
||||
const urgent = ref(false);
|
||||
const submitting = ref(false);
|
||||
|
||||
/** 三种下单方式:一口价 / 师傅报价竞价 / 用户自主定价 */
|
||||
const modes = [
|
||||
{ key: 'fixed', name: '一口价', desc: '平台明码标价' },
|
||||
{ key: 'quote', name: '师傅报价', desc: '师傅竞价,您来选' },
|
||||
{ key: 'custom', name: '自主定价', desc: '您出价,师傅接单' }
|
||||
];
|
||||
const priceType = ref('fixed');
|
||||
const userPrice = ref('');
|
||||
|
||||
const modeNote = computed(() => {
|
||||
if (priceType.value === 'quote') return '发布后进入报价大厅,师傅提交报价,您对比价格与评分后选择';
|
||||
if (priceType.value === 'custom') return '填写您愿意支付的总价(含楼层等),师傅认可即可接单';
|
||||
return '按平台标准价结算,支付后师傅先到先得';
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const pad = (n) => (n < 10 ? '0' + n : '' + n);
|
||||
const today = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
||||
@@ -100,19 +162,43 @@
|
||||
});
|
||||
|
||||
let serviceId = 0;
|
||||
let calcTimer = null;
|
||||
|
||||
async function calc() {
|
||||
if (!serviceId) return;
|
||||
const custom = priceType.value === 'custom';
|
||||
if (custom && (!userPrice.value || Number(userPrice.value) <= 0)) {
|
||||
amount.value = null;
|
||||
return;
|
||||
}
|
||||
const floor = address.value ? address.value.floor : form.floor;
|
||||
const elev = address.value ? address.value.has_elevator : form.has_elevator;
|
||||
try {
|
||||
amount.value = await api.calc({
|
||||
service_id: serviceId, floor: floor, has_elevator: elev, urgent: urgent.value ? 1 : 0
|
||||
service_id: serviceId,
|
||||
floor: floor,
|
||||
has_elevator: elev,
|
||||
urgent: urgent.value ? 1 : 0,
|
||||
price_type: priceType.value,
|
||||
user_price: custom ? Number(userPrice.value) : 0
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
watch([urgent, () => form.floor, () => form.has_elevator], () => calc());
|
||||
function scheduleCalc() {
|
||||
clearTimeout(calcTimer);
|
||||
calcTimer = setTimeout(calc, 300);
|
||||
}
|
||||
|
||||
function selectMode(key) {
|
||||
if (priceType.value === key) return;
|
||||
priceType.value = key;
|
||||
amount.value = null;
|
||||
calc();
|
||||
}
|
||||
|
||||
watch([urgent, () => form.floor, () => form.has_elevator, priceType], scheduleCalc);
|
||||
watch(userPrice, scheduleCalc);
|
||||
|
||||
function onDate(e) { date.value = e.detail.value; }
|
||||
function onTime(e) { time.value = e.detail.value; }
|
||||
@@ -168,6 +254,10 @@
|
||||
if (submitting.value) return;
|
||||
if (!isLogin()) { toLogin(); return; }
|
||||
if (!address.value) { uni.showToast({ title: '请先保存服务地址', icon: 'none' }); return; }
|
||||
if (priceType.value === 'custom' && (!userPrice.value || Number(userPrice.value) <= 0)) {
|
||||
uni.showToast({ title: '请填写您的出价', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
const appoint = Math.floor(new Date(date.value.replace(/-/g, '/') + ' ' + time.value + ':00').getTime() / 1000);
|
||||
@@ -175,13 +265,16 @@
|
||||
service_id: serviceId,
|
||||
address_id: address.value.id,
|
||||
appoint_time: appoint,
|
||||
urgent: urgent.value ? 1 : 0,
|
||||
urgent: priceType.value === 'custom' ? 0 : (urgent.value ? 1 : 0),
|
||||
goods_model: form.goods_model,
|
||||
goods_num: form.goods_num,
|
||||
remark: form.remark
|
||||
remark: form.remark,
|
||||
price_type: priceType.value,
|
||||
user_price: priceType.value === 'custom' ? Number(userPrice.value) : 0
|
||||
});
|
||||
await api.orderPay({ order_id: res.order_id, pay_type: 'mock' });
|
||||
uni.showToast({ title: '支付成功,等待师傅接单' });
|
||||
const tip = priceType.value === 'quote' ? '支付成功,等待师傅报价' : '支付成功,等待师傅接单';
|
||||
uni.showToast({ title: tip });
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({ url: '/pages/order/detail?id=' + res.order_id });
|
||||
}, 600);
|
||||
@@ -196,6 +289,9 @@
|
||||
serviceId = Number(options.service_id || 0);
|
||||
const res = await api.serviceDetail(serviceId);
|
||||
svc.value = res.detail;
|
||||
// 默认沿用服务价态:报价服务 → 师傅报价;区间服务 → 自主定价;其余 → 一口价
|
||||
priceType.value = svc.value.price_type === 'quote' ? 'quote'
|
||||
: (svc.value.price_type === 'range' ? 'custom' : 'fixed');
|
||||
await loadAddress();
|
||||
calc();
|
||||
});
|
||||
@@ -223,6 +319,26 @@
|
||||
.urgent.on .urgent-txt { color: #E36B0C; font-weight: 600; }
|
||||
.sec { margin: 22rpx 28rpx 0; padding: 22rpx 24rpx; background: #fff; border-radius: 20rpx; box-shadow: 0 6rpx 20rpx rgba(240,129,31,0.06); }
|
||||
.sec-title { font-size: 28rpx; font-weight: 700; color: #1F1F1F; margin-bottom: 10rpx; }
|
||||
.modes { display: flex; margin-top: 8rpx; }
|
||||
.mode {
|
||||
position: relative; flex: 1; min-width: 0; margin-right: 14rpx; padding: 20rpx 16rpx;
|
||||
border-radius: 16rpx; background: #F8F8F8; border: 2rpx solid transparent;
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
.mode:last-child { margin-right: 0; }
|
||||
.mode.on { background: #FFF3E6; border-color: #F0710D; }
|
||||
.mode-name { font-size: 27rpx; font-weight: 700; color: #444; }
|
||||
.mode.on .mode-name { color: #E36B0C; }
|
||||
.mode-desc { margin-top: 6rpx; font-size: 20rpx; color: #9A9A9A; line-height: 28rpx; }
|
||||
.mode.on .mode-desc { color: #C07A3A; }
|
||||
.mode-check {
|
||||
position: absolute; right: 10rpx; top: 8rpx; width: 30rpx; height: 30rpx; line-height: 30rpx;
|
||||
text-align: center; border-radius: 50%; background: #F0710D; color: #fff; font-size: 20rpx;
|
||||
}
|
||||
.mode-note {
|
||||
margin-top: 14rpx; padding: 14rpx 16rpx; border-radius: 12rpx;
|
||||
background: #FAFAFA; font-size: 22rpx; color: #8A8A8A; line-height: 32rpx;
|
||||
}
|
||||
.addr { display: flex; align-items: center; }
|
||||
.addr-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
||||
.addr-c { font-size: 28rpx; font-weight: 600; color: #222; }
|
||||
@@ -241,11 +357,15 @@
|
||||
.seg-i { padding: 0 26rpx; line-height: 52rpx; margin-right: 12rpx; border-radius: 26rpx; background: #F5F5F5; font-size: 24rpx; color: #666; }
|
||||
.seg-i.on { background: linear-gradient(135deg, #FFA93F 0%, #F0710D 100%); color: #fff; }
|
||||
.save-btn { margin-top: 20rpx; height: 76rpx; display: flex; align-items: center; justify-content: center; border-radius: 38rpx; background: #FFF3E6; color: #E36B0C; font-size: 26rpx; font-weight: 600; }
|
||||
.price-input-wrap { flex: 1; display: flex; align-items: center; }
|
||||
.price-unit { font-size: 32rpx; font-weight: 700; color: #F0710D; margin-right: 8rpx; }
|
||||
.price-input { flex: 1; }
|
||||
.li { display: flex; justify-content: space-between; padding: 14rpx 0; border-bottom: 1rpx solid #F4F4F4; }
|
||||
.li.total { border-bottom: none; }
|
||||
.k { font-size: 26rpx; color: #666; }
|
||||
.v { font-size: 26rpx; color: #333; }
|
||||
.v.big { font-size: 34rpx; font-weight: 700; color: #F0710D; }
|
||||
.fee-note { display: block; margin-top: 10rpx; font-size: 21rpx; color: #9A9A9A; line-height: 30rpx; }
|
||||
.bar {
|
||||
position: fixed; left: 0; right: 0; bottom: 0; height: 120rpx;
|
||||
padding: 0 28rpx; padding-bottom: env(safe-area-inset-bottom);
|
||||
@@ -263,4 +383,3 @@
|
||||
}
|
||||
.bar-btn.disabled { opacity: 0.7; }
|
||||
</style>
|
||||
|
||||
|
||||
@@ -30,11 +30,37 @@
|
||||
<view class="li" v-if="Number(order.urgent)"><text class="k">加急</text><text class="v">是(+20%)</text></view>
|
||||
</view>
|
||||
|
||||
<view class="card" v-if="order.price_type === 'quote'">
|
||||
<view class="card-head">
|
||||
<text class="card-title">师傅报价</text>
|
||||
<text class="card-refresh" @tap="load">刷新</text>
|
||||
</view>
|
||||
<view class="quote-tip" v-if="order.status === 2 && quotes.length">已有 {{ quotes.length }} 位师傅报价,选择后师傅将上门服务</view>
|
||||
<view class="quote-empty" v-if="!quotes.length">暂无师傅报价,请稍候…</view>
|
||||
<view class="quote-item" v-for="q in quotes" :key="q.id" :class="{ chosen: q.status === 'accept' }">
|
||||
<view class="q-top">
|
||||
<view class="q-master">
|
||||
<text class="q-name">{{ q.realname }}</text>
|
||||
<text class="q-meta">评分 {{ q.score }} · 完成 {{ q.order_count }} 单</text>
|
||||
</view>
|
||||
<text class="q-amt">¥{{ q.amount }}</text>
|
||||
</view>
|
||||
<text class="q-desc" v-if="q.description">{{ q.description }}</text>
|
||||
<view class="q-foot">
|
||||
<text class="q-expire">有效期至 {{ q.expire_text }}</text>
|
||||
<text class="q-status" :class="'qs-' + q.status">{{ q.status_text }}</text>
|
||||
<view class="q-choose" v-if="canChoose && q.status === 'wait'" @tap="chooseQuote(q)">选择该师傅</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="card-title">费用明细</view>
|
||||
<view class="li"><text class="k">基础价</text><text class="v">¥{{ order.base_amount }}</text></view>
|
||||
<view class="li"><text class="k">楼层费</text><text class="v">¥{{ order.floor_amount }}</text></view>
|
||||
<view class="li"><text class="k">计价方式</text><text class="v">{{ order.price_type_text }}</text></view>
|
||||
<view class="li"><text class="k">{{ order.price_type === 'custom' ? '您的出价' : (order.price_type === 'quote' && Number(order.master_id) > 0 ? '师傅报价' : '基础价') }}</text><text class="v">¥{{ order.base_amount }}</text></view>
|
||||
<view class="li" v-if="Number(order.floor_amount) > 0"><text class="k">楼层费</text><text class="v">¥{{ order.floor_amount }}</text></view>
|
||||
<view class="li" v-if="Number(order.urgent_amount) > 0"><text class="k">加急费</text><text class="v">¥{{ order.urgent_amount }}</text></view>
|
||||
<view class="li" v-if="Number(order.discount_amount) > 0"><text class="k">优惠券抵扣</text><text class="v">-¥{{ order.discount_amount }}</text></view>
|
||||
<view class="li"><text class="k">实付</text><text class="v big">¥{{ order.pay_amount }}</text></view>
|
||||
</view>
|
||||
|
||||
@@ -78,6 +104,7 @@
|
||||
const master = ref(null);
|
||||
const logs = ref([]);
|
||||
const media = ref([]);
|
||||
const quotes = ref([]);
|
||||
let orderId = 0;
|
||||
|
||||
const statusTip = computed(() => {
|
||||
@@ -85,12 +112,34 @@
|
||||
return order.value.status_tip || '';
|
||||
});
|
||||
|
||||
const canChoose = computed(() => {
|
||||
return !!(order.value && order.value.price_type === 'quote'
|
||||
&& Number(order.value.status) === 2 && Number(order.value.master_id) === 0
|
||||
&& quotes.value.some(q => q.status === 'wait'));
|
||||
});
|
||||
|
||||
async function load() {
|
||||
const res = await api.orderDetail(orderId);
|
||||
order.value = res.order;
|
||||
master.value = res.master;
|
||||
logs.value = res.logs || [];
|
||||
media.value = res.media || [];
|
||||
quotes.value = res.quotes || [];
|
||||
}
|
||||
|
||||
function chooseQuote(q) {
|
||||
uni.showModal({
|
||||
title: '选择服务师傅',
|
||||
content: `确认选择 ${q.realname} 的报价 ¥${q.amount}?选定后师傅将与您联系`,
|
||||
success: async (r) => {
|
||||
if (!r.confirm) return;
|
||||
try {
|
||||
await api.orderChooseQuote({ order_id: orderId, quote_id: q.id });
|
||||
toast('已选定师傅');
|
||||
load();
|
||||
} catch (e) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toast(t) { uni.showToast({ title: t, icon: 'none' }); }
|
||||
@@ -129,6 +178,26 @@
|
||||
.hero-tip { display: block; margin-top: 10rpx; font-size: 24rpx; color: rgba(255,255,255,0.9); }
|
||||
.card { margin: 22rpx 28rpx 0; padding: 24rpx; background: #fff; border-radius: 20rpx; box-shadow: 0 6rpx 20rpx rgba(240,129,31,0.06); }
|
||||
.card-title { font-size: 28rpx; font-weight: 700; color: #1F1F1F; margin-bottom: 12rpx; }
|
||||
.card-head { display: flex; align-items: center; justify-content: space-between; }
|
||||
.card-refresh { font-size: 22rpx; color: #F0710D; }
|
||||
.quote-tip { margin-bottom: 10rpx; padding: 12rpx 16rpx; border-radius: 12rpx; background: #FFF3E6; font-size: 22rpx; color: #C07A3A; line-height: 30rpx; }
|
||||
.quote-empty { padding: 30rpx 0; text-align: center; font-size: 24rpx; color: #9A9A9A; }
|
||||
.quote-item { padding: 18rpx 0; border-bottom: 1rpx solid #F4F4F4; }
|
||||
.quote-item:last-child { border-bottom: none; }
|
||||
.quote-item.chosen { background: #FFFBF6; border-radius: 12rpx; padding: 18rpx 14rpx; }
|
||||
.q-top { display: flex; align-items: flex-start; justify-content: space-between; }
|
||||
.q-master { display: flex; flex-direction: column; }
|
||||
.q-name { font-size: 28rpx; font-weight: 700; color: #222; }
|
||||
.q-meta { margin-top: 4rpx; font-size: 21rpx; color: #9A9A9A; }
|
||||
.q-amt { font-size: 34rpx; font-weight: 700; color: #F0710D; }
|
||||
.q-desc { display: block; margin-top: 10rpx; font-size: 23rpx; color: #666; line-height: 32rpx; }
|
||||
.q-foot { display: flex; align-items: center; margin-top: 10rpx; }
|
||||
.q-expire { flex: 1; font-size: 20rpx; color: #B5B5B5; }
|
||||
.q-status { font-size: 21rpx; color: #9A9A9A; margin-right: 14rpx; }
|
||||
.q-status.qs-wait { color: #4A7CFF; }
|
||||
.q-status.qs-accept { color: #2FA84F; }
|
||||
.q-status.qs-reject, .q-status.qs-expired { color: #B5B5B5; }
|
||||
.q-choose { padding: 0 24rpx; line-height: 56rpx; border-radius: 28rpx; color: #fff; font-size: 23rpx; font-weight: 600; background: linear-gradient(135deg,#FFA93F 0%,#F0710D 100%); }
|
||||
.li { display: flex; justify-content: space-between; padding: 12rpx 0; border-bottom: 1rpx solid #F4F4F4; }
|
||||
.k { flex-shrink: 0; width: 150rpx; font-size: 26rpx; color: #666; }
|
||||
.v { flex: 1; text-align: right; font-size: 26rpx; color: #333; line-height: 36rpx; }
|
||||
|
||||
@@ -40,7 +40,7 @@ class Order extends Model
|
||||
|
||||
public function getPriceTypeList()
|
||||
{
|
||||
return ['fixed' => __('Fixed'), 'quote' => __('Quote'), 'range' => __('Range')];
|
||||
return ['fixed' => __('Fixed'), 'quote' => __('Quote'), 'range' => __('Range'), 'custom' => __('用户自主定价')];
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ class Home extends Base
|
||||
|
||||
/**
|
||||
* 费用试算(基础价 + 楼层费 + 加急费 - 优惠)
|
||||
* 支持下单页实时切换:一口价 / 师傅报价 / 用户自主定价
|
||||
*/
|
||||
public function calc()
|
||||
{
|
||||
@@ -110,28 +111,48 @@ class Home extends Base
|
||||
'floor' => (int)$this->request->param('floor', 1),
|
||||
'has_elevator' => (int)$this->request->param('has_elevator', 1),
|
||||
'urgent' => (int)$this->request->param('urgent', 0),
|
||||
], (int)$this->request->param('coupon_id', 0), $this->uid());
|
||||
], (int)$this->request->param('coupon_id', 0), $this->uid(),
|
||||
(string)$this->request->param('price_type', ''),
|
||||
(float)$this->request->param('user_price', 0));
|
||||
|
||||
$this->success('', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 金额计算(下单与试算共用,保证前后端口径一致)
|
||||
*
|
||||
* @param array $service 服务行
|
||||
* @param array $params floor / has_elevator / urgent
|
||||
* @param int $couponId 用户优惠券 id
|
||||
* @param int $uid 用户 id
|
||||
* @param string $priceType 计价方式:fixed 一口价 / quote 师傅报价 / custom 用户自主定价
|
||||
* @param float $userPrice 用户自主定价金额(总价,含楼层/加急)
|
||||
*/
|
||||
public static function calcAmount($service, $params, $couponId = 0, $uid = 0)
|
||||
public static function calcAmount($service, $params, $couponId = 0, $uid = 0, $priceType = '', $userPrice = 0)
|
||||
{
|
||||
$allowTypes = ['fixed', 'quote', 'range', 'custom'];
|
||||
$type = in_array($priceType, $allowTypes, true) ? $priceType : $service['price_type'];
|
||||
|
||||
$base = (float)$service['base_price'];
|
||||
$floor = max(0, (int)$params['floor']);
|
||||
$freeFloor = (int)$service['free_floor'];
|
||||
$floorFee = (float)$service['floor_fee'];
|
||||
|
||||
$floorAmount = 0;
|
||||
if ((int)$params['has_elevator'] === 0 && $floor > $freeFloor) {
|
||||
$floorAmount = ($floor - $freeFloor) * $floorFee;
|
||||
}
|
||||
$urgentAmount = 0;
|
||||
|
||||
$urgentAmount = (int)$params['urgent'] === 1 ? round($base * 0.2, 2) : 0;
|
||||
$total = round($base + $floorAmount + $urgentAmount, 2);
|
||||
if ($type === 'custom') {
|
||||
// 用户自主定价:出价即订单总价,已包含楼层费/加急费,师傅按此价接单
|
||||
$base = max(0, round((float)$userPrice, 2));
|
||||
$total = $base;
|
||||
} else {
|
||||
$floor = max(0, (int)$params['floor']);
|
||||
$freeFloor = (int)$service['free_floor'];
|
||||
$floorFee = (float)$service['floor_fee'];
|
||||
|
||||
if ((int)$params['has_elevator'] === 0 && $floor > $freeFloor) {
|
||||
$floorAmount = ($floor - $freeFloor) * $floorFee;
|
||||
}
|
||||
|
||||
$urgentAmount = (int)$params['urgent'] === 1 ? round($base * 0.2, 2) : 0;
|
||||
$total = round($base + $floorAmount + $urgentAmount, 2);
|
||||
}
|
||||
|
||||
$discount = 0;
|
||||
$couponInfo = null;
|
||||
@@ -151,7 +172,8 @@ class Home extends Base
|
||||
}
|
||||
|
||||
return [
|
||||
'price_type' => $service['price_type'],
|
||||
'price_type' => $type,
|
||||
'user_price' => $type === 'custom' ? round($base, 2) : 0,
|
||||
'base_amount' => round($base, 2),
|
||||
'floor_amount' => round($floorAmount, 2),
|
||||
'urgent_amount' => round($urgentAmount, 2),
|
||||
@@ -159,8 +181,8 @@ class Home extends Base
|
||||
'discount_amount' => round($discount, 2),
|
||||
'pay_amount' => max(0, round($total - $discount, 2)),
|
||||
'coupon' => $couponInfo,
|
||||
'free_floor' => $freeFloor,
|
||||
'floor_fee' => round($floorFee, 2),
|
||||
'free_floor' => $type === 'custom' ? 0 : (int)$service['free_floor'],
|
||||
'floor_fee' => $type === 'custom' ? 0 : round((float)$service['floor_fee'], 2),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ class Master extends Base
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单大厅:支持排序与单类型筛选(一口价 / 报价 / 悬赏)
|
||||
* 订单大厅:支持排序与单类型筛选(一口价 / 师傅报价 / 用户自主定价)
|
||||
*/
|
||||
public function hall()
|
||||
{
|
||||
@@ -155,17 +155,20 @@ class Master extends Base
|
||||
$page = max(1, (int)$this->request->param('page', 1));
|
||||
$limit = min(30, max(1, (int)$this->request->param('limit', 10)));
|
||||
$sort = $this->request->param('sort', 'new'); // new 最新 / amount 报酬 / urgent 急单 / time 时效
|
||||
$type = $this->request->param('type', 'all'); // all / fixed 一口价 / quote 报价
|
||||
$type = $this->request->param('type', 'all'); // all / fixed 一口价 / quote 师傅报价 / custom 用户自主定价
|
||||
|
||||
$build = function () use ($catIds, $type) {
|
||||
$q = Db::name('order')->where('status', OrderStatus::PENDING_ACCEPT)->where('master_id', 0);
|
||||
$q = Db::name('order')->where('master_id', 0)
|
||||
->where('status', 'in', [OrderStatus::PENDING_ACCEPT, OrderStatus::QUOTING]);
|
||||
if (!empty($catIds)) {
|
||||
$q->where('cat_id', 'in', $catIds);
|
||||
}
|
||||
if ($type === 'fixed') {
|
||||
$q->where('price_type', 'fixed');
|
||||
$q->where('status', OrderStatus::PENDING_ACCEPT)->where('price_type', 'fixed');
|
||||
} elseif ($type === 'quote') {
|
||||
$q->where('price_type', 'quote');
|
||||
$q->where('status', 'in', [OrderStatus::PENDING_ACCEPT, OrderStatus::QUOTING])->where('price_type', 'quote');
|
||||
} elseif ($type === 'custom') {
|
||||
$q->where('status', OrderStatus::PENDING_ACCEPT)->where('price_type', 'custom');
|
||||
}
|
||||
return $q;
|
||||
};
|
||||
@@ -193,6 +196,10 @@ class Master extends Base
|
||||
$row['status_text'] = OrderStatus::label($row['status']);
|
||||
$row['left_seconds'] = max(0, (int)$row['createtime'] + 3600 - $now);
|
||||
$row['quote_count'] = (int)Db::name('order_quote')->where('order_id', $row['id'])->count();
|
||||
$row['price_type_text'] = $row['price_type'] === 'fixed' ? '一口价'
|
||||
: ($row['price_type'] === 'quote' ? '师傅报价'
|
||||
: ($row['price_type'] === 'custom' ? '用户自主定价' : '按面积计价'));
|
||||
$row['user_price'] = $row['price_type'] === 'custom' ? (float)$row['base_amount'] : 0;
|
||||
}
|
||||
unset($row);
|
||||
|
||||
@@ -217,6 +224,9 @@ class Master extends Base
|
||||
if (!$order) {
|
||||
$this->error('订单不存在');
|
||||
}
|
||||
if ($order['price_type'] === 'quote') {
|
||||
$this->error('该订单为师傅报价订单,请提交报价');
|
||||
}
|
||||
if ((int)$order['status'] !== OrderStatus::PENDING_ACCEPT || (int)$order['master_id'] !== 0) {
|
||||
$this->error('手慢了,该订单已被接走');
|
||||
}
|
||||
@@ -433,9 +443,15 @@ class Master extends Base
|
||||
if (!$order) {
|
||||
$this->error('订单不存在');
|
||||
}
|
||||
if ((int)$order['status'] !== OrderStatus::PENDING_ACCEPT) {
|
||||
if ($order['price_type'] !== 'quote') {
|
||||
$this->error('该订单不是报价竞价订单');
|
||||
}
|
||||
if (!in_array((int)$order['status'], [OrderStatus::PENDING_ACCEPT, OrderStatus::QUOTING], true)) {
|
||||
$this->error('该订单已不在报价阶段');
|
||||
}
|
||||
if ((int)$order['master_id'] > 0) {
|
||||
$this->error('该订单已选定师傅');
|
||||
}
|
||||
if ($amount <= 0) {
|
||||
$this->error('请填写报价金额');
|
||||
}
|
||||
@@ -453,6 +469,13 @@ class Master extends Base
|
||||
'status' => 'wait',
|
||||
'createtime' => time(),
|
||||
]);
|
||||
|
||||
// 兼容旧流程:已支付但仍在待接单的报价单,收到首个报价时进入「报价中」
|
||||
if ((int)$order['status'] === OrderStatus::PENDING_ACCEPT) {
|
||||
$this->transit($order, OrderStatus::QUOTING, '收到师傅报价,订单进入报价中', [], 'master', (int)$master['id']);
|
||||
}
|
||||
$this->notify('user', (int)$order['user_id'], '收到新报价', '师傅 ' . $master['realname'] . ' 对订单 ' . $order['order_no'] . ' 报价 ¥' . round($amount, 2) . ',请前往订单详情选择', 'order', $orderId);
|
||||
|
||||
$this->success('报价已提交');
|
||||
}
|
||||
|
||||
@@ -518,13 +541,17 @@ class Master extends Base
|
||||
->limit(50)
|
||||
->select();
|
||||
foreach ($rows as $row) {
|
||||
$toStatus = $row['price_type'] === 'quote' ? OrderStatus::QUOTING : OrderStatus::PENDING_ACCEPT;
|
||||
Db::name('order')->where('id', $row['id'])->update([
|
||||
'master_id' => 0,
|
||||
'status' => OrderStatus::PENDING_ACCEPT,
|
||||
'status' => $toStatus,
|
||||
'accept_time' => null,
|
||||
'updatetime' => time(),
|
||||
]);
|
||||
$this->addLog($row['id'], OrderStatus::PENDING_ACCEPT, '超 2 小时未联系客户,订单自动回流大厅', 'system', 0);
|
||||
if ($row['price_type'] === 'quote') {
|
||||
Db::name('order_quote')->where('order_id', $row['id'])->where('status', 'accept')->update(['status' => 'wait']);
|
||||
}
|
||||
$this->addLog($row['id'], $toStatus, $row['price_type'] === 'quote' ? '师傅超 2 小时未联系客户,订单重新开放报价' : '超 2 小时未联系客户,订单自动回流大厅', 'system', 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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] : '一口价';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class OrderStatus
|
||||
|
||||
/** 合法流转:from => [to, to, ...] */
|
||||
protected static $transitions = [
|
||||
self::PENDING_PAY => [self::PENDING_ACCEPT, self::CANCELLED],
|
||||
self::PENDING_PAY => [self::PENDING_ACCEPT, self::QUOTING, self::CANCELLED],
|
||||
self::PENDING_ACCEPT => [self::QUOTING, self::ACCEPTED, self::CANCELLED],
|
||||
self::QUOTING => [self::ACCEPTED, self::CANCELLED],
|
||||
self::ACCEPTED => [self::SCHEDULED, self::ARRIVED, self::PENDING_ACCEPT, self::CANCELLED],
|
||||
@@ -140,6 +140,6 @@ class OrderStatus
|
||||
*/
|
||||
public static function canCancelByUser($status)
|
||||
{
|
||||
return in_array((int)$status, [self::PENDING_PAY, self::PENDING_ACCEPT, self::ACCEPTED, self::SCHEDULED], true);
|
||||
return in_array((int)$status, [self::PENDING_PAY, self::PENDING_ACCEPT, self::QUOTING, self::ACCEPTED, self::SCHEDULED], true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ export default {
|
||||
orderVerify: (data) => post('/order/verify', data),
|
||||
orderList: (data) => get('/order/index', data),
|
||||
orderDetail: (id) => get('/order/detail', { id: id }),
|
||||
orderQuotes: (orderId) => get('/order/quotes', { order_id: orderId }),
|
||||
orderChooseQuote: (data) => post('/order/chooseQuote', data),
|
||||
|
||||
/* ---------- 师傅端 ---------- */
|
||||
masterApply: (data) => post('/master/apply', data),
|
||||
|
||||
在新工单中引用
屏蔽一个用户