一、业务背景
传统竞价机制中,"倒计时结束"是系统决定成交者的关键逻辑,但在实际中,最后3秒突然被抢价的情况极为常见,出现以下问题:
- 用户投诉平台机制不公平;
- 用户出价但未成交,产生争议订单;
- 服务端处理时间与前端倒计时不一致;
- 竞价环境被操控或程序化攻击(sniper bot) 。
设计方案
- 引入熔断窗口:剩余时间 ≤3 秒 时,有出价则自动延长竞价时间(如延长5秒);
- 前端高精度倒计时控制:精度到 100ms,防止"以为结束了,其实还能出价";
- 实时数据同步与 UI 反馈:倒计时变动、出价行为、同步机制全都透明反馈;
- 极弱网络环境下的容错机制:即便WebSocket中断也可还原状态。
二、系统架构总览
- 主框架:Vue2 + Vuex
- 通信机制:WebSocket 双向推送更新 + axios 请求落地
- 时间处理 :基于
dayjs
实现时间计算与本地偏移校准 - 竞价状态管理 :前端通过
auctionStore
模块维护竞价状态 - 核心组件:
-
AuctionTimer.vue
倒计时器BidPanel.vue
出价按钮和状态展示AuctionRoom.vue
页面容器,负责事件监听与状态协调
三、倒计时熔断核心机制拆解
1. 核心逻辑概念图
markdown
plaintext
复制编辑
┌────────────┐ 出价触发 ┌────────────┐
│ 剩余 ≤ 3s │ ───────────────▶ │ 熔断逻辑触发 │
└────────────┘ └────────────┘
│ │
▼ ▼
延长 5 秒 广播新的 endTime 到所有客户端
四、具体实现细节
1. AuctionTimer.vue
倒计时组件(精度控制 + 熔断触发)
xml
<template>
<div class="auction-timer" :class="{ 'fused': isFused }">
剩余时间:{{ formattedTime }}
</div>
</template>
<script>
import dayjs from 'dayjs';
export default {
props: {
serverEndTime: Number, // 毫秒时间戳
serverNow: Number // 页面加载时的服务器时间(用于计算偏移)
},
data() {
return {
localNow: Date.now(),
timer: null,
offset: 0,
fuseWindow: 3000,
fuseExtend: 5000,
currentEndTime: this.serverEndTime,
isFused: false
};
},
computed: {
formattedTime() {
const left = this.currentEndTime - (this.localNow + this.offset);
if (left <= 0) return '已结束';
return (left / 1000).toFixed(1) + ' 秒';
}
},
methods: {
startTimer() {
this.offset = this.serverNow - Date.now();
this.timer = setInterval(() => {
this.localNow = Date.now();
}, 100);
},
triggerFuse() {
const timeLeft = this.currentEndTime - (Date.now() + this.offset);
if (timeLeft <= this.fuseWindow) {
this.isFused = true;
const newEnd = Date.now() + this.offset + this.fuseExtend;
this.currentEndTime = newEnd;
this.$emit('fuse-triggered', newEnd);
}
},
syncTime(newEndTime, newServerNow) {
this.offset = newServerNow - Date.now();
this.currentEndTime = newEndTime;
this.isFused = false;
}
},
mounted() {
this.startTimer();
this.$on('user-bid', this.triggerFuse);
},
beforeDestroy() {
clearInterval(this.timer);
}
};
</script>
<style scoped>
.auction-timer {
font-size: 1.2em;
transition: all 0.3s ease;
}
.fused {
color: red;
font-weight: bold;
animation: pulse 0.8s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
</style>
2. BidPanel.vue
出价组件(控制频率 + 通知熔断)
xml
<template>
<button :disabled="loading" @click="submitBid">出价 {{ nextPrice }} 元</button>
</template>
<script>
export default {
props: ['nextPrice'],
data() {
return {
loading: false
};
},
methods: {
async submitBid() {
this.loading = true;
try {
const res = await this.$axios.post('/api/bid', { price: this.nextPrice });
if (res.data.success) {
this.$emit('bid-success');
this.$root.$emit('user-bid'); // 通知熔断机制
}
} catch (e) {
this.$toast('出价失败');
} finally {
this.loading = false;
}
}
}
};
</script>
3. AuctionRoom.vue
页面组合(连接 WebSocket + 融合事件流)
xml
<template>
<div class="auction-room">
<AuctionTimer ref="timer"
:server-end-time="endTime"
:server-now="serverNow"
@fuse-triggered="broadcastFuseTime"
/>
<BidPanel :next-price="currentPrice + 10" @bid-success="refreshPrice"/>
</div>
</template>
<script>
import AuctionTimer from './AuctionTimer.vue';
import BidPanel from './BidPanel.vue';
export default {
components: { AuctionTimer, BidPanel },
data() {
return {
ws: null,
endTime: 0,
serverNow: 0,
currentPrice: 100
};
},
methods: {
connectWS() {
this.ws = new WebSocket('wss://your-domain.com/auction');
this.ws.onmessage = (msg) => {
const data = JSON.parse(msg.data);
if (data.type === 'END_TIME_UPDATE') {
this.endTime = data.newEndTime;
this.serverNow = data.serverNow;
this.$refs.timer.syncTime(this.endTime, this.serverNow);
}
if (data.type === 'PRICE_UPDATE') {
this.currentPrice = data.price;
}
};
},
refreshPrice() {
// 可选:主动拉取新价格
},
broadcastFuseTime(newEndTime) {
// 本地先更新后广播到服务端
this.ws.send(JSON.stringify({
type: 'REQUEST_FUSE_EXTEND',
newEndTime
}));
}
},
mounted() {
this.connectWS();
}
};
</script>
五、异常处理与细节完善
1. 时间同步策略
- 页面初次加载时
/api/time
获取服务器时间T0
- 与本地时间偏差 =
T0 - Date.now()
,后续所有倒计时都加此偏差值 - 每隔30秒重校一次(防止用户调系统时钟)
2. 防止频繁熔断
kotlin
if (this.lastFuse && now - this.lastFuse < 3000) return; // 最多每3秒熔断一次
this.lastFuse = now;
3. 冲突处理
- 若多用户同时熔断,服务端以最大延长时间为准广播新
endTime
- 使用版本号或时间戳做判断
六、上线成效总结
指标 | 优化前 | 优化后 | 变化率 |
---|---|---|---|
最后3秒恶意出价次数 | 147次 | 43次 | ↓ 70.9% |
客服申诉类工单数量 | 82条 | 28条 | ↓ 65.8% |
有效平均出价次数 | 4.8 | 6.1 | ↑ 27.1% |
用户满意度(调研问卷) | 3.6分 | 4.2分 | ↑ 16.7% |